diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3f010a4c37b0..184dc4cb5367 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -30,7 +30,9 @@ jobs: - name: Install pandoc run: sudo apt install pandoc - name: Install Flower dependencies (mandatory only) - run: python -m poetry install --extras "simulation" + run: | + python -m poetry install --extras "simulation" + python -m pip install ./dev - name: Install Flower Datasets run: | cd datasets diff --git a/.github/workflows/framework.yml b/.github/workflows/framework.yml index a8ff69204b58..9ad59f0c5c50 100644 --- a/.github/workflows/framework.yml +++ b/.github/workflows/framework.yml @@ -38,7 +38,9 @@ jobs: with: python-version: ${{ matrix.python }} - name: Install dependencies (mandatory only) - run: python -m poetry install --all-extras + run: | + python -m poetry install --all-extras + python -m pip install ./dev - name: Check if protos need recompilation run: ./dev/check-protos.sh - name: Lint + Test (isort/black/docformatter/mypy/pylint/flake8/pytest) diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml index 47bb4b284136..a5c01fff7c80 100644 --- a/.github/workflows/pr_check.yml +++ b/.github/workflows/pr_check.yml @@ -22,4 +22,6 @@ jobs: python-version: 3.11 poetry-skip: 'true' - name: Check PR title format - run: python ./dev/check_pr_title.py "${{ github.event.pull_request.title }}" + run: | + python -m pip install ./dev + flwr-dev check-title "${{ github.event.pull_request.title }}" diff --git a/dev/build-docs.sh b/dev/build-docs.sh index f4bf958b0ebf..e00189ca32a6 100755 --- a/dev/build-docs.sh +++ b/dev/build-docs.sh @@ -8,7 +8,7 @@ cd $ROOT ./dev/build-baseline-docs.sh cd $ROOT -python dev/build-example-docs.py +flwr-dev build-examples cd $ROOT ./datasets/dev/build-flwr-datasets-docs.sh diff --git a/dev/check-protos.sh b/dev/check-protos.sh index 4e9927bbab0b..45679f355ac0 100755 --- a/dev/check-protos.sh +++ b/dev/check-protos.sh @@ -22,7 +22,7 @@ cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/../ # but did not recompile or commit the new proto python files # Recompile protos -python -m flwr_tool.protoc +flwr-dev compile-protos # Fail if user forgot to recompile CHANGED=$(git diff --name-only HEAD src/py/flwr/proto) diff --git a/src/py/flwr_tool/__init__.py b/dev/flwr_dev/__init__.py similarity index 100% rename from src/py/flwr_tool/__init__.py rename to dev/flwr_dev/__init__.py diff --git a/dev/flwr_dev/app.py b/dev/flwr_dev/app.py new file mode 100644 index 000000000000..954271af2e99 --- /dev/null +++ b/dev/flwr_dev/app.py @@ -0,0 +1,50 @@ +# Copyright 2024 Flower Labs GmbH. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Flower command line interface.""" + +import typer +from typer.main import get_command + +from flwr_dev.build_docker_image_matrix import build_images +from flwr_dev.build_example_docs import build_examples +from flwr_dev.check_copyright import check_copyrights +from flwr_dev.check_pr_title import check_title +from flwr_dev.init_py_check import check_init +from flwr_dev.init_py_fix import fix_init +from flwr_dev.protoc import compile_protos +from flwr_dev.update_changelog import generate_changelog + +cli = typer.Typer( + help=typer.style( + "flwr is the Flower command line interface.", + fg=typer.colors.BRIGHT_YELLOW, + bold=True, + ), + no_args_is_help=True, +) + +cli.command()(check_title) +cli.command()(build_examples) +cli.command()(build_images) +cli.command()(check_copyrights) +cli.command()(check_init) +cli.command()(fix_init) +cli.command()(compile_protos) +cli.command()(generate_changelog) + +typer_click_object = get_command(cli) + +if __name__ == "__main__": + cli() diff --git a/dev/build-docker-image-matrix.py b/dev/flwr_dev/build_docker_image_matrix.py similarity index 56% rename from dev/build-docker-image-matrix.py rename to dev/flwr_dev/build_docker_image_matrix.py index 9d255ac5471f..11eab915469f 100644 --- a/dev/build-docker-image-matrix.py +++ b/dev/flwr_dev/build_docker_image_matrix.py @@ -1,5 +1,7 @@ -""" -Usage: python dev/build-docker-image-matrix.py --flwr-version +"""Build Docker image matrix. + +Usage: +python dev/build-docker-image-matrix.py --flwr-version Images are built in three workflows: stable, nightly, and unstable (main). Each builds for `amd64` and `arm64`. @@ -9,37 +11,52 @@ - Ubuntu uses `glibc`, compatible with most ML frameworks. 2. **Alpine Images**: - - Used only for minimal images (e.g., SuperLink) where no extra dependencies are expected. - - Limited use due to dependency (in particular ML frameworks) compilation complexity with `musl`. + - Used only for minimal images (e.g., SuperLink) + where no extra dependencies are expected. + - Limited use due to dependency (in particular ML frameworks) + compilation complexity with `musl`. Workflow Details: -- **Stable Release**: Triggered on new releases. Builds full matrix (all Python versions, Ubuntu and Alpine). -- **Nightly Release**: Daily trigger. Builds full matrix (latest Python, Ubuntu only). -- **Unstable**: Triggered on main branch commits. Builds simplified matrix (latest Python, Ubuntu only). +- **Stable Release**: Triggered on new releases. + Builds full matrix (all Python versions, Ubuntu and Alpine). +- **Nightly Release**: Daily trigger. + Builds full matrix (latest Python, Ubuntu only). +- **Unstable**: Triggered on main branch commits. + Builds simplified matrix (latest Python, Ubuntu only). """ -import sys -import argparse import json from dataclasses import asdict, dataclass, field -from enum import Enum -from typing import Any, Callable, Dict, List, Optional + +try: + from enum import StrEnum + + class _DistroName(StrEnum): + ALPINE = "alpine" + UBUNTU = "ubuntu" + +except ImportError: + from enum import Enum + + class _DistroName(str, Enum): + ALPINE = "alpine" + UBUNTU = "ubuntu" + + +from typing import Any, Callable, Optional + +import typer # when we switch to Python 3.11 in the ci, we need to change the DistroName to: # class DistroName(StrEnum): # ALPINE = "alpine" # UBUNTU = "ubuntu" -assert sys.version_info < (3, 11), "Script requires Python 3.9 or lower." - - -class DistroName(str, Enum): - ALPINE = "alpine" - UBUNTU = "ubuntu" +# assert sys.version_info < (3, 11), "Script requires Python 3.9 or lower." @dataclass -class Distro: - name: "DistroName" +class _Distro: + name: "_DistroName" version: str @@ -54,18 +71,18 @@ class Distro: @dataclass -class Variant: - distro: Distro +class _Variant: + distro: _Distro extras: Optional[Any] = None @dataclass -class CpuVariant: +class _CpuVariant: pass @dataclass -class CudaVariant: +class _CudaVariant: version: str @@ -75,41 +92,41 @@ class CudaVariant: ("12.1.0", "22.04"), ("12.3.2", "22.04"), ] -LATEST_SUPPORTED_CUDA_VERSION = Variant( - Distro(DistroName.UBUNTU, "22.04"), - CudaVariant(version="12.4.1"), +LATEST_SUPPORTED_CUDA_VERSION = _Variant( + _Distro(_DistroName.UBUNTU, "22.04"), + _CudaVariant(version="12.4.1"), ) # ubuntu base image -UBUNTU_VARIANT = Variant( - Distro(DistroName.UBUNTU, "24.04"), - CpuVariant(), +UBUNTU_VARIANT = _Variant( + _Distro(_DistroName.UBUNTU, "24.04"), + _CpuVariant(), ) # alpine base image -ALPINE_VARIANT = Variant( - Distro(DistroName.ALPINE, "3.19"), - CpuVariant(), +ALPINE_VARIANT = _Variant( + _Distro(_DistroName.ALPINE, "3.19"), + _CpuVariant(), ) # ubuntu cuda base images CUDA_VARIANTS = [ - Variant( - Distro(DistroName.UBUNTU, ubuntu_version), - CudaVariant(version=cuda_version), + _Variant( + _Distro(_DistroName.UBUNTU, ubuntu_version), + _CudaVariant(version=cuda_version), ) for (cuda_version, ubuntu_version) in CUDA_VERSIONS_CONFIG ] + [LATEST_SUPPORTED_CUDA_VERSION] -def remove_patch_version(version: str) -> str: +def _remove_patch_version(version: str) -> str: return ".".join(version.split(".")[0:2]) @dataclass -class BaseImageBuilder: +class _BaseImageBuilder: # pylint: disable=too-many-instance-attributes file_dir_fn: Callable[[Any], str] tags_fn: Callable[[Any], list[str]] build_args_fn: Callable[[Any], str] @@ -121,92 +138,98 @@ class BaseImageBuilder: @dataclass -class BaseImage(BaseImageBuilder): +class _BaseImage(_BaseImageBuilder): namespace_repository: str = "flwr/base" @property def file_dir(self) -> str: + """File directory.""" return self.file_dir_fn(self.build_args) @property - def tags(self) -> str: + def tags(self) -> list[str]: + """Get list of tags.""" return self.tags_fn(self.build_args) @property def tags_encoded(self) -> str: + """Encoded tags.""" return "\n".join(self.tags) @property def build_args_encoded(self) -> str: + """Build arguments.""" return self.build_args_fn(self.build_args) @dataclass -class BinaryImage: +class _BinaryImage: namespace_repository: str file_dir: str base_image: str tags_encoded: str -def new_binary_image( +def _new_binary_image( name: str, - base_image: BaseImage, + base_image: _BaseImage, tags_fn: Optional[Callable], -) -> Dict[str, Any]: +) -> _BinaryImage: tags = [] if tags_fn is not None: tags += tags_fn(base_image) or [] - return BinaryImage( - f"flwr/{name}", - f"{DOCKERFILE_ROOT}/{name}", - base_image.tags[0], - "\n".join(tags), + return _BinaryImage( + namespace_repository=f"flwr/{name}", + file_dir=f"{DOCKERFILE_ROOT}/{name}", + base_image=base_image.tags[0], + tags_encoded="\n".join(tags), ) -def generate_binary_images( +def _generate_binary_images( name: str, - base_images: List[BaseImage], + base_images: list[_BaseImage], tags_fn: Optional[Callable] = None, - filter: Optional[Callable] = None, -) -> List[Dict[str, Any]]: - filter = filter or (lambda _: True) + filter_func: Optional[Callable] = None, +) -> list[_BinaryImage]: + filter_func = filter_func or (lambda _: True) return [ - new_binary_image(name, image, tags_fn) for image in base_images if filter(image) + _new_binary_image(name, image, tags_fn) + for image in base_images + if filter_func(image) ] -def tag_latest_alpine_with_flwr_version(image: BaseImage) -> List[str]: +def _tag_latest_alpine_with_flwr_version(image: _BaseImage) -> list[str]: if ( - image.build_args.variant.distro.name == DistroName.ALPINE + image.build_args.variant.distro.name == _DistroName.ALPINE and image.build_args.python_version == LATEST_SUPPORTED_PYTHON_VERSION ): return image.tags + [image.build_args.flwr_version] - else: - return image.tags + return image.tags -def tag_latest_ubuntu_with_flwr_version(image: BaseImage) -> List[str]: +def _tag_latest_ubuntu_with_flwr_version(image: _BaseImage) -> list[str]: if ( - image.build_args.variant.distro.name == DistroName.UBUNTU + image.build_args.variant.distro.name == _DistroName.UBUNTU and image.build_args.python_version == LATEST_SUPPORTED_PYTHON_VERSION - and isinstance(image.build_args.variant.extras, CpuVariant) + and isinstance(image.build_args.variant.extras, _CpuVariant) ): return image.tags + [image.build_args.flwr_version] - else: - return image.tags + return image.tags # # Build matrix for stable releases # -def build_stable_matrix(flwr_version: str) -> List[BaseImage]: +def _build_stable_matrix( + flwr_version: str, +) -> tuple[list[_BaseImage], list[_BinaryImage]]: @dataclass - class StableBaseImageBuildArgs: - variant: Variant + class _StableBaseImageBuildArgs: + variant: _Variant python_version: str flwr_version: str @@ -217,19 +240,22 @@ class StableBaseImageBuildArgs: """ cpu_build_args_variants = [ - StableBaseImageBuildArgs(UBUNTU_VARIANT, python_version, flwr_version) + _StableBaseImageBuildArgs(UBUNTU_VARIANT, python_version, flwr_version) for python_version in SUPPORTED_PYTHON_VERSIONS ] + [ - StableBaseImageBuildArgs( + _StableBaseImageBuildArgs( ALPINE_VARIANT, LATEST_SUPPORTED_PYTHON_VERSION, flwr_version ) ] cpu_base_images = [ - BaseImage( - file_dir_fn=lambda args: f"{DOCKERFILE_ROOT}/base/{args.variant.distro.name.value}", + _BaseImage( + file_dir_fn=lambda args: ( + f"{DOCKERFILE_ROOT}/base/{args.variant.distro.name.value}" + ), tags_fn=lambda args: [ - f"{args.flwr_version}-py{args.python_version}-{args.variant.distro.name.value}{args.variant.distro.version}" + f"{args.flwr_version}-py{args.python_version}-" + f"{args.variant.distro.name.value}{args.variant.distro.version}" ], build_args_fn=lambda args: cpu_build_args.format( python_version=args.python_version, @@ -243,18 +269,22 @@ class StableBaseImageBuildArgs: ] cuda_build_args_variants = [ - StableBaseImageBuildArgs(variant, python_version, flwr_version) + _StableBaseImageBuildArgs(variant, python_version, flwr_version) for variant in CUDA_VARIANTS for python_version in SUPPORTED_PYTHON_VERSIONS ] cuda_build_args = cpu_build_args + """CUDA_VERSION={cuda_version}""" - cuda_base_image = [ - BaseImage( - file_dir_fn=lambda args: f"{DOCKERFILE_ROOT}/base/{args.variant.distro.name.value}-cuda", + _cuda_base_image = [ + _BaseImage( + file_dir_fn=lambda args: ( + f"{DOCKERFILE_ROOT}/base/{args.variant.distro.name.value}-cuda" + ), tags_fn=lambda args: [ - f"{args.flwr_version}-py{args.python_version}-cu{remove_patch_version(args.variant.extras.version)}-{args.variant.distro.name.value}{args.variant.distro.version}", + f"{args.flwr_version}-py{args.python_version}-" + f"cu{_remove_patch_version(args.variant.extras.version)}-" + f"{args.variant.distro.name.value}{args.variant.distro.version}", ], build_args_fn=lambda args: cuda_build_args.format( python_version=args.python_version, @@ -273,41 +303,41 @@ class StableBaseImageBuildArgs: binary_images = ( # ubuntu and alpine images for the latest supported python version - generate_binary_images( + _generate_binary_images( "superlink", base_images, - tag_latest_alpine_with_flwr_version, + _tag_latest_alpine_with_flwr_version, lambda image: image.build_args.python_version == LATEST_SUPPORTED_PYTHON_VERSION - and isinstance(image.build_args.variant.extras, CpuVariant), + and isinstance(image.build_args.variant.extras, _CpuVariant), ) # ubuntu images for each supported python version - + generate_binary_images( + + _generate_binary_images( "supernode", base_images, - tag_latest_alpine_with_flwr_version, + _tag_latest_alpine_with_flwr_version, lambda image: ( - image.build_args.variant.distro.name == DistroName.UBUNTU - and isinstance(image.build_args.variant.extras, CpuVariant) + image.build_args.variant.distro.name == _DistroName.UBUNTU + and isinstance(image.build_args.variant.extras, _CpuVariant) ) or ( - image.build_args.variant.distro.name == DistroName.ALPINE + image.build_args.variant.distro.name == _DistroName.ALPINE and image.build_args.python_version == LATEST_SUPPORTED_PYTHON_VERSION ), ) # ubuntu images for each supported python version - + generate_binary_images( + + _generate_binary_images( "serverapp", base_images, - tag_latest_ubuntu_with_flwr_version, - lambda image: image.build_args.variant.distro.name == DistroName.UBUNTU, + _tag_latest_ubuntu_with_flwr_version, + lambda image: image.build_args.variant.distro.name == _DistroName.UBUNTU, ) # ubuntu images for each supported python version - + generate_binary_images( + + _generate_binary_images( "clientapp", base_images, - tag_latest_ubuntu_with_flwr_version, - lambda image: image.build_args.variant.distro.name == DistroName.UBUNTU, + _tag_latest_ubuntu_with_flwr_version, + lambda image: image.build_args.variant.distro.name == _DistroName.UBUNTU, ) ) @@ -317,14 +347,16 @@ class StableBaseImageBuildArgs: # # Build matrix for unstable releases # -def build_unstable_matrix(flwr_version_ref: str) -> List[BaseImage]: +def _build_unstable_matrix( + flwr_version_ref: str, +) -> tuple[list[_BaseImage], list[_BinaryImage]]: @dataclass - class UnstableBaseImageBuildArgs: - variant: Variant + class _UnstableBaseImageBuildArgs: + variant: _Variant python_version: str flwr_version_ref: str - cpu_ubuntu_build_args_variant = UnstableBaseImageBuildArgs( + cpu_ubuntu_build_args_variant = _UnstableBaseImageBuildArgs( UBUNTU_VARIANT, LATEST_SUPPORTED_PYTHON_VERSION, flwr_version_ref ) @@ -334,8 +366,10 @@ class UnstableBaseImageBuildArgs: DISTRO_VERSION={distro_version} """ - cpu_base_image = BaseImage( - file_dir_fn=lambda args: f"{DOCKERFILE_ROOT}/base/{args.variant.distro.name.value}", + cpu_base_image = _BaseImage( + file_dir_fn=lambda args: ( + f"{DOCKERFILE_ROOT}/base/{args.variant.distro.name.value}" + ), tags_fn=lambda _: ["unstable"], build_args_fn=lambda args: cpu_build_args.format( python_version=args.python_version, @@ -346,14 +380,16 @@ class UnstableBaseImageBuildArgs: build_args=cpu_ubuntu_build_args_variant, ) - cuda_build_args_variant = UnstableBaseImageBuildArgs( + cuda_build_args_variant = _UnstableBaseImageBuildArgs( LATEST_SUPPORTED_CUDA_VERSION, LATEST_SUPPORTED_PYTHON_VERSION, flwr_version_ref ) cuda_build_args = cpu_build_args + """CUDA_VERSION={cuda_version}""" - cuda_base_image = BaseImage( - file_dir_fn=lambda args: f"{DOCKERFILE_ROOT}/base/{args.variant.distro.name.value}-cuda", + _cuda_base_image = _BaseImage( + file_dir_fn=lambda args: ( + f"{DOCKERFILE_ROOT}/base/{args.variant.distro.name.value}-cuda" + ), tags_fn=lambda _: ["unstable-cuda"], build_args_fn=lambda args: cuda_build_args.format( python_version=args.python_version, @@ -369,20 +405,20 @@ class UnstableBaseImageBuildArgs: base_images = [cpu_base_image] binary_images = ( - generate_binary_images( + _generate_binary_images( "superlink", base_images, lambda image: image.tags, - lambda image: isinstance(image.build_args.variant.extras, CpuVariant), + lambda image: isinstance(image.build_args.variant.extras, _CpuVariant), ) - + generate_binary_images( + + _generate_binary_images( "supernode", base_images, lambda image: image.tags, - lambda image: isinstance(image.build_args.variant.extras, CpuVariant), + lambda image: isinstance(image.build_args.variant.extras, _CpuVariant), ) - + generate_binary_images("serverapp", base_images, lambda image: image.tags) - + generate_binary_images("clientapp", base_images, lambda image: image.tags) + + _generate_binary_images("serverapp", base_images, lambda image: image.tags) + + _generate_binary_images("clientapp", base_images, lambda image: image.tags) ) return base_images, binary_images @@ -391,15 +427,17 @@ class UnstableBaseImageBuildArgs: # # Build matrix for nightly releases # -def build_nightly_matrix(flwr_version: str, flwr_package: str) -> List[BaseImage]: +def _build_nightly_matrix( + flwr_version: str, flwr_package: str +) -> tuple[list[_BaseImage], list[_BinaryImage]]: @dataclass - class NightlyBaseImageBuildArgs: - variant: Variant + class _NightlyBaseImageBuildArgs: + variant: _Variant python_version: str flwr_version: str flwr_package: str - cpu_ubuntu_build_args_variant = NightlyBaseImageBuildArgs( + cpu_ubuntu_build_args_variant = _NightlyBaseImageBuildArgs( UBUNTU_VARIANT, LATEST_SUPPORTED_PYTHON_VERSION, flwr_version, flwr_package ) @@ -410,8 +448,10 @@ class NightlyBaseImageBuildArgs: DISTRO_VERSION={distro_version} """ - cpu_base_image = BaseImage( - file_dir_fn=lambda args: f"{DOCKERFILE_ROOT}/base/{args.variant.distro.name.value}", + cpu_base_image = _BaseImage( + file_dir_fn=lambda args: ( + f"{DOCKERFILE_ROOT}/base/{args.variant.distro.name.value}" + ), tags_fn=lambda args: [args.flwr_version, "nightly"], build_args_fn=lambda args: cpu_build_args.format( python_version=args.python_version, @@ -423,7 +463,7 @@ class NightlyBaseImageBuildArgs: build_args=cpu_ubuntu_build_args_variant, ) - cuda_build_args_variant = NightlyBaseImageBuildArgs( + cuda_build_args_variant = _NightlyBaseImageBuildArgs( LATEST_SUPPORTED_CUDA_VERSION, LATEST_SUPPORTED_PYTHON_VERSION, flwr_version, @@ -432,8 +472,10 @@ class NightlyBaseImageBuildArgs: cuda_build_args = cpu_build_args + """CUDA_VERSION={cuda_version}""" - cuda_base_image = BaseImage( - file_dir_fn=lambda args: f"{DOCKERFILE_ROOT}/base/{args.variant.distro.name.value}-cuda", + _cuda_base_image = _BaseImage( + file_dir_fn=lambda args: ( + f"{DOCKERFILE_ROOT}/base/{args.variant.distro.name.value}-cuda" + ), tags_fn=lambda args: [f"{args.flwr_version}-cuda", "nightly-cuda"], build_args_fn=lambda args: cuda_build_args.format( python_version=args.python_version, @@ -450,69 +492,63 @@ class NightlyBaseImageBuildArgs: base_images = [cpu_base_image] binary_images = ( - generate_binary_images( + _generate_binary_images( "superlink", base_images, lambda image: image.tags, - lambda image: isinstance(image.build_args.variant.extras, CpuVariant), + lambda image: isinstance(image.build_args.variant.extras, _CpuVariant), ) - + generate_binary_images( + + _generate_binary_images( "supernode", base_images, lambda image: image.tags, - lambda image: isinstance(image.build_args.variant.extras, CpuVariant), + lambda image: isinstance(image.build_args.variant.extras, _CpuVariant), ) - + generate_binary_images("serverapp", base_images, lambda image: image.tags) - + generate_binary_images("clientapp", base_images, lambda image: image.tags) + + _generate_binary_images("serverapp", base_images, lambda image: image.tags) + + _generate_binary_images("clientapp", base_images, lambda image: image.tags) ) return base_images, binary_images -if __name__ == "__main__": - arg_parser = argparse.ArgumentParser( - description="Generate Github Docker workflow matrix" - ) - arg_parser.add_argument("--flwr-version", type=str, required=True) - arg_parser.add_argument("--flwr-package", type=str, default="flwr") - arg_parser.add_argument( - "--matrix", choices=["stable", "nightly", "unstable"], default="stable" - ) - - args = arg_parser.parse_args() - - flwr_version = args.flwr_version - flwr_package = args.flwr_package - matrix = args.matrix - +def build_images( + flwr_version: str = typer.Option(..., help="The Flower version"), + flwr_package: str = typer.Option("flwr", help="The Flower package"), + matrix: str = typer.Option( + "stable", + help="The workflow matrix type", + case_sensitive=False, + ), +): + """Build updated docker images.""" if matrix == "stable": - base_images, binary_images = build_stable_matrix(flwr_version) + base_images, binary_images = _build_stable_matrix(flwr_version) elif matrix == "nightly": - base_images, binary_images = build_nightly_matrix(flwr_version, flwr_package) + base_images, binary_images = _build_nightly_matrix(flwr_version, flwr_package) else: - base_images, binary_images = build_unstable_matrix(flwr_version) + base_images, binary_images = _build_unstable_matrix(flwr_version) print( json.dumps( { "base": { - "images": list( - map( - lambda image: asdict( - image, - dict_factory=lambda x: { - k: v - for (k, v) in x - if v is not None and callable(v) is False - }, - ), - base_images, + "images": [ + asdict( + image, + dict_factory=lambda x: { + k: v + for (k, v) in x + if v is not None and callable(v) is False + }, ) - ) - }, - "binary": { - "images": list(map(lambda image: asdict(image), binary_images)) + for image in base_images + ] }, + "binary": {"images": [asdict(image) for image in binary_images]}, } ) ) + + +if __name__ == "__main__": + typer.run(build_images) diff --git a/dev/build-example-docs.py b/dev/flwr_dev/build_example_docs.py similarity index 89% rename from dev/build-example-docs.py rename to dev/flwr_dev/build_example_docs.py index 05656967bbbd..4f9e3039052d 100644 --- a/dev/build-example-docs.py +++ b/dev/flwr_dev/build_example_docs.py @@ -20,10 +20,12 @@ import subprocess from pathlib import Path -ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +from flwr_dev.common import get_git_root + +ROOT = get_git_root() INDEX = os.path.join(ROOT, "examples", "doc", "source", "index.rst") -initial_text = """ +INITIAL_TEXT = """ Flower Examples Documentation ----------------------------- @@ -50,15 +52,15 @@ """ -table_headers = ( +TABLE_HEADERS = ( "\n.. list-table::\n :widths: 50 15 15 15\n " ":header-rows: 1\n\n * - Title\n - Framework\n - Dataset\n - Tags\n\n" ) categories = { - "quickstart": {"table": table_headers, "list": ""}, - "advanced": {"table": table_headers, "list": ""}, - "other": {"table": table_headers, "list": ""}, + "quickstart": {"table": TABLE_HEADERS, "list": ""}, + "advanced": {"table": TABLE_HEADERS, "list": ""}, + "other": {"table": TABLE_HEADERS, "list": ""}, } urls = { @@ -104,7 +106,10 @@ "Oxford Flower-102": "https://www.robots.ox.ac.uk/~vgg/data/flowers/102/", "SpeechCommands": "https://huggingface.co/datasets/google/speech_commands", "Titanic": "https://www.kaggle.com/competitions/titanic", - "Waltons": "https://lifelines.readthedocs.io/en/latest/lifelines.datasets.html#lifelines.datasets.load_waltons", + "Waltons": ( + "https://lifelines.readthedocs.io/en/latest/lifelines.datasets.html" + "#lifelines.datasets.load_waltons" + ), } @@ -114,17 +119,15 @@ def _convert_to_link(search_result): for part in search_result.split(","): result += f"{_convert_to_link(part)}, " return result[:-2] - else: - search_result = search_result.strip() - name, url = search_result, urls.get(search_result, None) - if url: - return f"`{name.strip()} <{url.strip()}>`_" - else: - return search_result + search_result = search_result.strip() + name, url = search_result, urls.get(search_result, None) + if url: + return f"`{name.strip()} <{url.strip()}>`_" + return search_result def _read_metadata(example): - with open(os.path.join(example, "README.md")) as f: + with open(os.path.join(example, "README.md"), encoding="utf-8") as f: content = f.read() metadata_match = re.search(r"^---(.*?)^---", content, re.DOTALL | re.MULTILINE) @@ -186,9 +189,12 @@ def _copy_markdown_files(example): def _add_gh_button(example): - gh_text = f'[View on GitHub](https://github.com/adap/flower/blob/main/examples/{example})' + gh_text = ( + '[View on GitHub]' + f"(https://github.com/adap/flower/blob/main/examples/{example})" + ) readme_file = os.path.join(ROOT, "examples", "doc", "source", example + ".md") - with open(readme_file, "r+") as f: + with open(readme_file, "r+", encoding="utf-8") as f: content = f.read() if gh_text not in content: content = re.sub( @@ -224,8 +230,8 @@ def _main(): if os.path.exists(INDEX): os.remove(INDEX) - with open(INDEX, "w") as index_file: - index_file.write(initial_text) + with open(INDEX, "w", encoding="utf-8") as index_file: + index_file.write(INITIAL_TEXT) examples_dir = os.path.join(ROOT, "examples") for example in sorted(os.listdir(examples_dir)): @@ -239,7 +245,7 @@ def _main(): if not _add_table_entry(example_path, "advanced", "advanced"): _add_table_entry(example_path, "", "other") - with open(INDEX, "a") as index_file: + with open(INDEX, "a", encoding="utf-8") as index_file: index_file.write(categories["quickstart"]["table"]) index_file.write("\nAdvanced Examples\n-----------------\n") @@ -278,6 +284,11 @@ def _main(): index_file.write("\n") -if __name__ == "__main__": +def build_examples(): + """Update and build example docs.""" _main() subprocess.call(f"cd {ROOT}/examples/doc && make html", shell=True) + + +if __name__ == "__main__": + build_examples() diff --git a/src/py/flwr_tool/check_copyright.py b/dev/flwr_dev/check_copyright.py similarity index 87% rename from src/py/flwr_tool/check_copyright.py rename to dev/flwr_dev/check_copyright.py index 96870ba67bd0..aa158af3d852 100755 --- a/src/py/flwr_tool/check_copyright.py +++ b/dev/flwr_dev/check_copyright.py @@ -10,9 +10,11 @@ import subprocess import sys from pathlib import Path -from typing import List +from typing import Annotated, List -from flwr_tool.init_py_check import get_init_dir_list_and_warnings +import typer + +from flwr_dev.init_py_check import get_init_dir_list_and_warnings COPYRIGHT_FORMAT = """# Copyright {} Flower Labs GmbH. All Rights Reserved. # @@ -64,13 +66,20 @@ def _check_copyright(dir_list: List[str]) -> None: sys.exit(1) +def check_copyrights( + paths: Annotated[list[str], typer.Argument(help="Path of the files to analyze")] +): + """Verify that copyright notices are correct.""" + for path in paths: + abs_path: str = os.path.abspath(os.path.join(os.getcwd(), path)) + __, init_dirs = get_init_dir_list_and_warnings(abs_path) + _check_copyright(init_dirs) + + if __name__ == "__main__": if len(sys.argv) == 0: raise Exception( # pylint: disable=W0719 "Please provide at least one directory path relative " "to your current working directory." ) - for i, _ in enumerate(sys.argv): - abs_path: str = os.path.abspath(os.path.join(os.getcwd(), sys.argv[i])) - __, init_dirs = get_init_dir_list_and_warnings(abs_path) - _check_copyright(init_dirs) + check_copyrights(sys.argv) diff --git a/dev/check_pr_title.py b/dev/flwr_dev/check_pr_title.py similarity index 88% rename from dev/check_pr_title.py rename to dev/flwr_dev/check_pr_title.py index b4fcccafc6f5..d9355ebb4762 100644 --- a/dev/check_pr_title.py +++ b/dev/flwr_dev/check_pr_title.py @@ -17,17 +17,23 @@ import pathlib import re import sys -import tomllib +from typing import Annotated -if __name__ == "__main__": +import tomli +import typer + +from flwr_dev.common import get_git_root - pr_title = sys.argv[1] +def check_title( + pr_title: Annotated[str, typer.Argument(help="Title of the PR to check")] +): + """Check if the title of a PR is valid.""" # Load the YAML configuration - with (pathlib.Path(__file__).parent.resolve() / "changelog_config.toml").open( + with (pathlib.Path(get_git_root()) / "dev" / "changelog_config.toml").open( "rb" ) as file: - config = tomllib.load(file) + config = tomli.load(file) # Extract types, project, and scope from the config types = "|".join(config["type"]) @@ -73,3 +79,7 @@ "the changelog:\n\n\t`feat(framework:skip) Add new option to build CLI`\n" ) sys.exit(1) + + +if __name__ == "__main__": + check_title(sys.argv[1]) diff --git a/dev/flwr_dev/common.py b/dev/flwr_dev/common.py new file mode 100644 index 000000000000..59b019fb8f65 --- /dev/null +++ b/dev/flwr_dev/common.py @@ -0,0 +1,15 @@ +"""Provide useful common functions.""" + +import subprocess + + +def get_git_root(): + """Obtain the root of the git repo.""" + return ( + subprocess.Popen( + ["git", "rev-parse", "--show-toplevel"], stdout=subprocess.PIPE + ) + .communicate()[0] + .rstrip() + .decode("utf-8") + ) diff --git a/src/py/flwr_tool/fix_copyright.py b/dev/flwr_dev/fix_copyright.py similarity index 79% rename from src/py/flwr_tool/fix_copyright.py rename to dev/flwr_dev/fix_copyright.py index a5bbbdf616f7..0b0361924a43 100755 --- a/src/py/flwr_tool/fix_copyright.py +++ b/dev/flwr_dev/fix_copyright.py @@ -9,10 +9,12 @@ import os import sys from pathlib import Path -from typing import List +from typing import Annotated, List -from flwr_tool.check_copyright import COPYRIGHT_FORMAT, _get_file_creation_year -from flwr_tool.init_py_check import get_init_dir_list_and_warnings +import typer + +from flwr_dev.check_copyright import COPYRIGHT_FORMAT, _get_file_creation_year +from flwr_dev.init_py_check import get_init_dir_list_and_warnings def _insert_or_edit_copyright(py_file: Path) -> None: @@ -47,13 +49,20 @@ def _fix_copyright(dir_list: List[str]) -> None: _insert_or_edit_copyright(py_file) +def fix_copyrights( + paths: Annotated[list[str], typer.Argument(help="Path of the files to analyze")] +): + """Modify files to add valid copyright notices.""" + for path in paths: + abs_path: str = os.path.abspath(os.path.join(os.getcwd(), path)) + __, init_dirs = get_init_dir_list_and_warnings(abs_path) + _fix_copyright(init_dirs) + + if __name__ == "__main__": if len(sys.argv) == 0: raise Exception( # pylint: disable=W0719 "Please provide at least one directory path relative " "to your current working directory." ) - for i, _ in enumerate(sys.argv): - abs_path: str = os.path.abspath(os.path.join(os.getcwd(), sys.argv[i])) - __, init_dirs = get_init_dir_list_and_warnings(abs_path) - _fix_copyright(init_dirs) + fix_copyrights(sys.argv) diff --git a/src/py/flwr_tool/init_py_check.py b/dev/flwr_dev/init_py_check.py similarity index 92% rename from src/py/flwr_tool/init_py_check.py rename to dev/flwr_dev/init_py_check.py index 1fb08513bb6a..5159ac720847 100755 --- a/src/py/flwr_tool/init_py_check.py +++ b/dev/flwr_dev/init_py_check.py @@ -11,7 +11,9 @@ import re import sys from pathlib import Path -from typing import List, Tuple +from typing import Annotated, List, Tuple + +import typer def get_init_dir_list_and_warnings(absolute_path: str) -> Tuple[List[str], List[str]]: @@ -96,13 +98,20 @@ def check_all_init_files(dir_list: List[str]) -> None: sys.exit(1) +def check_init( + paths: Annotated[list[str], typer.Argument(help="Path of the files to analyze")] +): + """Check if __init__ files are sorted correctly.""" + for path in paths: + abs_path: str = os.path.abspath(os.path.join(os.getcwd(), path)) + init_dirs = check_missing_init_files(abs_path) + check_all_init_files(init_dirs) + + if __name__ == "__main__": if len(sys.argv) == 0: raise Exception( # pylint: disable=W0719 "Please provide at least one directory path relative " "to your current working directory." ) - for i, _ in enumerate(sys.argv): - abs_path: str = os.path.abspath(os.path.join(os.getcwd(), sys.argv[i])) - init_dirs = check_missing_init_files(abs_path) - check_all_init_files(init_dirs) + check_init(sys.argv) diff --git a/src/py/flwr_tool/init_py_fix.py b/dev/flwr_dev/init_py_fix.py similarity index 83% rename from src/py/flwr_tool/init_py_fix.py rename to dev/flwr_dev/init_py_fix.py index 5ad27829ae8e..50853b5d4e3c 100755 --- a/src/py/flwr_tool/init_py_fix.py +++ b/dev/flwr_dev/init_py_fix.py @@ -8,11 +8,12 @@ import os import sys -from typing import List +from typing import Annotated, List import black +import typer -from flwr_tool.init_py_check import get_all_var_list, get_init_dir_list_and_warnings +from flwr_dev.init_py_check import get_all_var_list, get_init_dir_list_and_warnings def fix_all_init_files(dir_list: List[str]) -> None: @@ -57,13 +58,20 @@ def fix_all_init_files(dir_list: List[str]) -> None: print(warning) +def fix_init( + paths: Annotated[list[str], typer.Argument(help="Path of the files to analyze")] +): + """Sort variables inside __init__.py files.""" + for path in paths: + abs_path: str = os.path.abspath(os.path.join(os.getcwd(), path)) + _, init_dirs = get_init_dir_list_and_warnings(abs_path) + fix_all_init_files(init_dirs) + + if __name__ == "__main__": if len(sys.argv) == 0: raise Exception( # pylint: disable=W0719 "Please provide at least one directory path relative " "to your current working directory." ) - for i, _ in enumerate(sys.argv): - abs_path: str = os.path.abspath(os.path.join(os.getcwd(), sys.argv[i])) - warnings, init_dirs = get_init_dir_list_and_warnings(abs_path) - fix_all_init_files(init_dirs) + fix_init(sys.argv) diff --git a/src/py/flwr_tool/protoc.py b/dev/flwr_dev/protoc.py similarity index 89% rename from src/py/flwr_tool/protoc.py rename to dev/flwr_dev/protoc.py index b0b078c2eae4..117813498771 100644 --- a/src/py/flwr_tool/protoc.py +++ b/dev/flwr_dev/protoc.py @@ -21,15 +21,17 @@ import grpc_tools from grpc_tools import protoc +from flwr_dev.common import get_git_root + GRPC_PATH = grpc_tools.__path__[0] -DIR_PATH = path.dirname(path.realpath(__file__)) -IN_PATH = path.normpath(f"{DIR_PATH}/../../proto") -OUT_PATH = path.normpath(f"{DIR_PATH}/..") +ROOT = get_git_root() +IN_PATH = path.normpath(f"{ROOT}/src/proto") +OUT_PATH = path.normpath(f"{ROOT}/src/py") PROTO_FILES = glob.glob(f"{IN_PATH}/flwr/**/*.proto") -def compile_all() -> None: +def compile_protos() -> None: """Compile all protos in the `src/proto` directory. The directory structure of the `src/proto` directory will be mirrored in `src/py`. @@ -55,4 +57,4 @@ def compile_all() -> None: if __name__ == "__main__": - compile_all() + compile_protos() diff --git a/src/py/flwr_tool/protoc_test.py b/dev/flwr_dev/protoc_test.py similarity index 100% rename from src/py/flwr_tool/protoc_test.py rename to dev/flwr_dev/protoc_test.py diff --git a/dev/update_changelog.py b/dev/flwr_dev/update_changelog.py similarity index 89% rename from dev/update_changelog.py rename to dev/flwr_dev/update_changelog.py index 0b4359d90e13..7f100e47a560 100644 --- a/dev/update_changelog.py +++ b/dev/flwr_dev/update_changelog.py @@ -23,35 +23,23 @@ import tomllib except ModuleNotFoundError: import tomli as tomllib + from datetime import date from sys import argv -from typing import Optional +from typing import Annotated, Optional +import typer from github import Github from github.PullRequest import PullRequest from github.Repository import Repository from github.Tag import Tag +from flwr_dev.common import get_git_root + REPO_NAME = "adap/flower" CHANGELOG_FILE = "doc/source/ref-changelog.md" CHANGELOG_SECTION_HEADER = "### Changelog entry" -# Load the TOML configuration -with (pathlib.Path(__file__).parent.resolve() / "changelog_config.toml").open( - "rb" -) as file: - CONFIG = tomllib.load(file) - -# Extract types, project, and scope from the config -TYPES = "|".join(CONFIG["type"]) -PROJECTS = "|".join(CONFIG["project"]) + "|\\*" -SCOPE = CONFIG["scope"] -ALLOWED_VERBS = CONFIG["allowed_verbs"] - -# Construct the pattern -PATTERN_TEMPLATE = CONFIG["pattern_template"] -PATTERN = PATTERN_TEMPLATE.format(types=TYPES, projects=PROJECTS, scope=SCOPE) - def _get_latest_tag(gh_api: Github) -> tuple[Repository, Optional[Tag]]: """Retrieve the latest tag from the GitHub repository.""" @@ -140,10 +128,11 @@ def _format_pr_reference(title: str, number: int, url: str) -> str: def _extract_changelog_entry( pr_info: PullRequest, + pattern: str, ) -> dict[str, str]: """Extract the changelog entry from a pull request's body.""" # Use regex search to find matches - match = re.search(PATTERN, pr_info.title) + match = re.search(pattern, pr_info.title) if match: # Extract components from the regex groups pr_type = match.group(1) @@ -167,7 +156,7 @@ def _extract_changelog_entry( } -def _update_changelog(prs: set[PullRequest]) -> bool: +def _update_changelog(prs: set[PullRequest], pattern: str) -> bool: """Update the changelog file with entries from provided pull requests.""" breaking_changes = False unknown_changes = False @@ -187,7 +176,7 @@ def _update_changelog(prs: set[PullRequest]) -> bool: ) for pr_info in prs: - parsed_title = _extract_changelog_entry(pr_info) + parsed_title = _extract_changelog_entry(pr_info, pattern) # Skip if PR should be skipped or already in changelog if ( @@ -268,23 +257,42 @@ def _bump_minor_version(tag: Tag) -> Optional[str]: match = re.match(r"v(\d+)\.(\d+)\.(\d+)", tag.name) if match is None: return None - major, minor, _ = [int(x) for x in match.groups()] + major, minor, _ = (int(x) for x in match.groups()) # Increment the minor version and reset patch version new_version = f"v{major}.{minor + 1}.0" return new_version -def main() -> None: +def generate_changelog( + gh_token: Annotated[ + str, typer.Argument(help="A GitHub API token with read access.") + ] +): """Update changelog using the descriptions of PRs since the latest tag.""" # Initialize GitHub Client with provided token (as argument) - gh_api = Github(argv[1]) + + # Load the TOML configuration + with (pathlib.Path(get_git_root()) / "dev" / "changelog_config.toml").open( + "rb" + ) as file: + config = tomllib.load(file) + + # Extract types, project, and scope from the config + types = "|".join(config["type"]) + projects = "|".join(config["project"]) + "|\\*" + scope = config["scope"] + + # Construct the pattern + pattern_template = config["pattern_template"] + pattern = pattern_template.format(types=types, projects=projects, scope=scope) + gh_api = Github(gh_token) repo, latest_tag = _get_latest_tag(gh_api) if not latest_tag: print("No tags found in the repository.") return shortlog, prs = _get_pull_requests_since_tag(repo, latest_tag) - if _update_changelog(prs): + if _update_changelog(prs, pattern): new_version = _bump_minor_version(latest_tag) if not new_version: print("Wrong tag format.") @@ -294,4 +302,4 @@ def main() -> None: if __name__ == "__main__": - main() + generate_changelog(argv[1]) diff --git a/dev/update_python.py b/dev/flwr_dev/update_python.py similarity index 100% rename from dev/update_python.py rename to dev/flwr_dev/update_python.py diff --git a/dev/update_version.py b/dev/flwr_dev/update_version.py similarity index 95% rename from dev/update_version.py rename to dev/flwr_dev/update_version.py index 0b2db3369a3d..890f52657655 100644 --- a/dev/update_version.py +++ b/dev/flwr_dev/update_version.py @@ -5,7 +5,6 @@ import sys from pathlib import Path - REPLACE_CURR_VERSION = { "doc/source/conf.py": [ ".. |stable_flwr_version| replace:: {version}", @@ -79,13 +78,14 @@ def _update_versions(file_patterns, replace_strings, new_version, check): return wrong -if __name__ == "__main__": +# pylint: disable=too-many-branches +def _main(): conf_path = Path("doc/source/conf.py") if not conf_path.is_file(): raise FileNotFoundError(f"{conf_path} not found!") - content = conf_path.read_text() + content = conf_path.read_text(encoding="utf-8") # Search for the current non-updated version match = re.search(r"\.\.\s*\|stable_flwr_version\|\s*replace::\s*(\S+)", content) @@ -95,7 +95,10 @@ def _update_versions(file_patterns, replace_strings, new_version, check): ) parser.add_argument( "--old_version", - help="Current (non-updated) version of the package, soon to be the old version.", + help=( + "Current (non-updated) version of the package, " + "soon to be the old version." + ), default=match.group(1) if match else None, ) parser.add_argument( @@ -149,3 +152,7 @@ def _update_versions(file_patterns, replace_strings, new_version, check): if wrong and args.check: sys.exit("Some version haven't been updated.") + + +if __name__ == "__main__": + _main() diff --git a/dev/format.sh b/dev/format.sh index a3129b932e5d..b2d6252fcdd3 100755 --- a/dev/format.sh +++ b/dev/format.sh @@ -5,8 +5,8 @@ cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/../ taplo fmt # Python -python -m flwr_tool.check_copyright src/py/flwr -python -m flwr_tool.init_py_fix src/py/flwr +flwr-dev check-copyrights src/py/flwr +flwr-dev fix-init src/py/flwr python -m isort --skip src/py/flwr/proto src/py python -m black -q --exclude src/py/flwr/proto src/py python -m docformatter -i -r src/py/flwr -e src/py/flwr/proto diff --git a/dev/pyproject.toml b/dev/pyproject.toml new file mode 100644 index 000000000000..5b729332fd09 --- /dev/null +++ b/dev/pyproject.toml @@ -0,0 +1,25 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "flwr-dev" +version = "1.0.0" +requires-python = ">=3.9" +authors = [{ name = "The Flower Authors", email = "hello@flower.ai" }] +description = "Small utility compiling useful scripts" +license = "Apache-2.0" +dependencies = [ + "typer", + "grpcio-tools==1.60.0", + "protobuf>=4.25.2,<5.0.0", + "mypy-protobuf==3.2.0", + "types-protobuf==3.19.18", + "black==24.2.0", + "PyGithub==2.1.1", + "tomli>=2.0.1,<3.0.0", +] +packages = [{ include = "flwr_dev", from = "." }] + +[project.scripts] +flwr-dev = "flwr_dev.app:cli" diff --git a/dev/test-tool.sh b/dev/test-tool.sh index a6b7d3efc326..b97cbbf39964 100755 --- a/dev/test-tool.sh +++ b/dev/test-tool.sh @@ -4,9 +4,9 @@ cd "$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"/../ echo "=== test-tool.sh ===" -python -m isort --check-only src/py/flwr_tool && echo "- isort: done" && -python -m black --check src/py/flwr_tool && echo "- black: done" && +python -m isort --check-only dev/flwr_dev && echo "- isort: done" && +python -m black --check dev/flwr_dev && echo "- black: done" && # mypy is covered by test.sh -python -m pylint src/py/flwr_tool && echo "- pylint: done" && +python -m pylint dev/flwr_dev && echo "- pylint: done" && # python -m pytest -q src/py/flwr_tool && echo "- pytest: done" && echo "- All Python checks passed" diff --git a/dev/test.sh b/dev/test.sh index b8eeed14bc46..0068474c3714 100755 --- a/dev/test.sh +++ b/dev/test.sh @@ -19,7 +19,7 @@ python -m black --exclude "src\/py\/flwr\/proto" --check src/py/flwr benchmarks echo "- black: done" echo "- init_py_check: start" -python -m flwr_tool.init_py_check src/py/flwr src/py/flwr_tool +flwr-dev check-init src/py/flwr src/py/flwr_tool echo "- init_py_check: done" echo "- docformatter: start" @@ -79,7 +79,7 @@ echo "- All rST checks passed" echo "- Start license checks" echo "- copyright: start" -python -m flwr_tool.check_copyright src/py/flwr +flwr-dev check-copyrights src/py/flwr echo "- copyright: done" echo "- licensecheck: start" diff --git a/pyproject.toml b/pyproject.toml index f1207a94c448..3c82cd6947af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -152,7 +152,7 @@ extend_exclude = [ [tool.isort] profile = "black" -known_first_party = ["flwr", "flwr_tool"] +known_first_party = ["flwr", "flwr_tool", "flwr_dev"] [tool.black] line-length = 88