diff --git a/README.md b/README.md index 148a4fa..6a10dac 100644 --- a/README.md +++ b/README.md @@ -109,16 +109,28 @@ Please read these guides in order to learn how to organize your repository for m ## General command usage -Unless a project name is specified, most commands will operate on all projects in a git repository based on the current working folder: +Unless a project name is specified, commands will operate on all projects in a git repository based on the current working folder: - `stew ` - Perform a command on all projects - `stew --help` - Obtain help about a particular command + + +Some commands allow specifying a project name: + - `stew ` - - Perform the command on all projects with `` in their name (partial match) + - Perform the command on all projects with `` in their name (partial, case-insensitive) + - e.g.: `stew ci tools` will run on `tools`, `tools-common`, `my-tools`, etc. - `stew --exact-match` - Disable partial project name matching + - e.g.: `stew ci tools --exact-match` will run on `tools` but not on `tools-common`, `my-tools`, etc. +- `stew .` + - **v3.1.3** Only consider the project at this location + - The path must start with `.` + - Nested projects will not run + - e.g.: `stew ci .` will only run on the project in the current folder + - e.g.: `stew ci ./tools` will only run on the project in the `tools` folder The main commands are explained below. diff --git a/coveo_stew/commands.py b/coveo_stew/commands.py index 2d2bf3e..199f137 100644 --- a/coveo_stew/commands.py +++ b/coveo_stew/commands.py @@ -3,7 +3,7 @@ import re from collections import defaultdict from pathlib import Path -from typing import Generator, Iterable, Set, Tuple, Union +from typing import Generator, Iterable, Optional, Set, Tuple, Union import click from coveo_functools.finalizer import finalizer @@ -11,7 +11,7 @@ from coveo_systools.filesystem import find_repo_root from coveo_stew.ci.runner_status import RunnerStatus -from coveo_stew.discovery import discover_pyprojects, find_pyproject +from coveo_stew.discovery import Predicate, discover_pyprojects, find_pyproject from coveo_stew.exceptions import ( CheckFailed, PythonProjectNotFound, @@ -47,7 +47,7 @@ def _pull_dev_requirements( ) -> Generator[Path, None, None]: """Writes the dev-dependencies of pydev projects' local dependencies into pydev's pyproject.toml file.""" dry_run_text = "(dry run) " if dry_run else "" - for pydev_project in discover_pyprojects(predicate=is_pydev_project, verbose=verbose): + for pydev_project in _discover_pyprojects(predicate=is_pydev_project, verbose=verbose): echo.step(f"Analyzing dev requirements for {pydev_project}") if pull_and_write_dev_requirements(pydev_project, dry_run=dry_run): echo.outcome( @@ -76,7 +76,7 @@ def check_outdated(verbose: bool = False) -> None: echo.step("Analyzing all pyproject.toml files and artifacts:") outdated: Set[Path] = set() try: - for project in discover_pyprojects(verbose=verbose): + for project in _discover_pyprojects(verbose=verbose): echo.noise(project, item=True) if not project.lock_path.exists() or project.lock_is_outdated(): outdated.add(project.lock_path) @@ -109,7 +109,7 @@ def fix_outdated(verbose: bool = False) -> None: updated: Set[Path] = set() with finalizer(_echo_updated, updated): try: - for project in discover_pyprojects(verbose=verbose): + for project in _discover_pyprojects(verbose=verbose): echo.noise(project, item=True) if project.lock_if_needed(): updated.add(project.lock_path) @@ -130,7 +130,7 @@ def bump(verbose: bool = False) -> None: updated: Set[Path] = set() with finalizer(_echo_updated, updated): try: - for project in discover_pyprojects(verbose=verbose): + for project in _discover_pyprojects(verbose=verbose): echo.step(f"Bumping {project.lock_path}") if project.bump(): updated.add(project.toml_path) @@ -204,8 +204,9 @@ def fresh_eggs(project_name: str = None, verbose: bool = False) -> None: """ echo.step("Removing *.egg-info folders.") deleted = False + try: - for project in discover_pyprojects(query=project_name, verbose=verbose): + for project in _discover_pyprojects(query=project_name, verbose=verbose): if project.remove_egg_info(): echo.outcome("Deleted: ", project.egg_path, item=True) deleted = True @@ -265,7 +266,7 @@ def locate(project_name: str, verbose: bool = False) -> None: # check for partial matches to guide the user partial_matches = ( project.package.name - for project in discover_pyprojects(query=project_name, verbose=verbose) + for project in _discover_pyprojects(query=project_name, verbose=verbose) ) try: raise ExitWithFailure( @@ -287,7 +288,7 @@ def refresh(project_name: str = None, exact_match: bool = False, verbose: bool = echo.step("Refreshing python project environments...") pydev_projects = [] try: - for project in discover_pyprojects( + for project in _discover_pyprojects( query=project_name, exact_match=exact_match, verbose=verbose ): if project.options.pydev: @@ -342,7 +343,7 @@ def ci( ) -> None: failures = defaultdict(list) try: - for project in discover_pyprojects( + for project in _discover_pyprojects( query=project_name, exact_match=exact_match, verbose=verbose ): echo.step(project.package.name, pad_after=False) @@ -375,3 +376,23 @@ def ci( raise ExitWithFailure(failures=projects, exit_code=exit_code) from CheckFailed( f"{len(failures)} project(s) failed ci steps." ) + + +def _discover_pyprojects( + query: Optional[str] = None, + predicate: Optional[Predicate] = None, + exact_match: bool = False, + verbose: bool = False, +) -> Generator[PythonProject, None, None]: + if query and query.startswith("."): + if exact_match: + echo.warning( + f"--exact-match only works with project names, but we are targeting a path: {query}" + ) + yield from discover_pyprojects( + path=Path(query), verbose=verbose, find_nested=False, predicate=predicate + ) + else: + yield from discover_pyprojects( + query=query, exact_match=exact_match, verbose=verbose, predicate=predicate + ) diff --git a/coveo_stew/discovery.py b/coveo_stew/discovery.py index dd2d6cd..5316a4c 100644 --- a/coveo_stew/discovery.py +++ b/coveo_stew/discovery.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Callable, Generator +from typing import Callable, Generator, Optional from coveo_styles.styles import echo from coveo_systools.filesystem import find_paths, find_repo_root @@ -25,20 +25,25 @@ def find_pyproject(project_name: str, path: Path = None, *, verbose: bool = Fals def discover_pyprojects( path: Path = None, *, - query: str = None, + query: Optional[str] = None, exact_match: bool = False, verbose: bool = False, - predicate: Predicate = None, + predicate: Optional[Predicate] = None, + find_nested: bool = True, ) -> Generator[PythonProject, None, None]: """ Search for Python projects in a path and return PythonProject instances. Parameters: path: where to start looking for pyproject.toml files. Default: git root or '.' - query: substring for package selection. '-' and '_' are equivalent. case insensitive. + query: Name of the project: + - case-insensitive + - Substring match if exact_match is false. + - '-' and '_' are equivalent. exact_match: turns query into an exact match. Recommended use: CI scripts verbose: output more details to command line predicate: optional inclusion filter + find_nested: search in subdirectories """ if not path: path = find_repo_root(default=".") @@ -47,7 +52,9 @@ def discover_pyprojects( raise PythonProjectNotFound("An exact match was requested but no query was provided.") predicate = predicate or (lambda _: True) - paths = find_paths(PythonFile.PyProjectToml, search_from=path, in_children=True, in_root=True) + paths = find_paths( + PythonFile.PyProjectToml, search_from=path, in_children=find_nested, in_root=True + ) count_projects = 0 for file in paths: