diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ae917dcd..adce991c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,8 +17,6 @@ uses: "./.github/workflows/lint.yml" variants: uses: "./.github/workflows/variants.yml" - typecheck: - uses: "./.github/workflows/typecheck.yml" # Always build & lint package. build-package: @@ -27,7 +25,6 @@ - lint - tests - variants - - typecheck runs-on: ubuntu-latest permissions: attestations: write diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml deleted file mode 100644 index 32f9a8a9..00000000 --- a/.github/workflows/typecheck.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: Type checks - -on: - push: - workflow_call: - workflow_dispatch: - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - - - name: Set up uv - uses: astral-sh/setup-uv@v7 - with: - enable-cache: true - cache-dependency-glob: "pyproject.toml" - - - name: Set up Python 3.13 - run: uv python install 3.13 - - - name: Install Project - run: make install - - - name: Run Typechecks - run: make typecheck diff --git a/CHANGES.md b/CHANGES.md index 2b1d40e6..e3ca06dd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,12 @@ # Changelog +## 2.2.0 + +- Feature: Add `qa.ty` domain for Astral's ty type checker. + ty is an extremely fast Python type checker (10-100x faster than mypy). + Registers with both CHECK_TARGETS and TYPECHECK_TARGETS for fast feedback. + [jensens] + ## 2.1.0 - Enhancement: Use tables in the generated sphinx code for topic/domains. diff --git a/Makefile b/Makefile index 59bf154c..c703f83e 100644 --- a/Makefile +++ b/Makefile @@ -8,11 +8,9 @@ #: core.mxfiles #: core.packages #: docs.sphinx -#: qa.coverage -#: qa.isort -#: qa.mypy #: qa.ruff #: qa.test +#: qa.ty # # SETTINGS (ALL CHANGES MADE BELOW SETTINGS WILL BE LOST) ############################################################################## @@ -111,12 +109,6 @@ MXMAKE?=-e . # Default: src RUFF_SRC?=src -## qa.isort - -# Source folder to scan for Python files to run isort on. -# Default: src -ISORT_SRC?=src - ## docs.sphinx # Documentation source folder. @@ -148,6 +140,17 @@ PROJECT_CONFIG?=mx.ini # Default: false PACKAGES_ALLOW_PRERELEASES?=false +## qa.ty + +# Source folder for type checking. +# Default: src +TY_SRC?=src + +# Target Python version for type checking (e.g., 3.12). +# Leave empty to use default detection. +# No default value. +TY_PYTHON_VERSION?= + ## qa.test # The command which gets executed. Defaults to the location the @@ -164,23 +167,6 @@ TEST_REQUIREMENTS?=pytest # No default value. TEST_DEPENDENCY_TARGETS?= -## qa.coverage - -# The command which gets executed. Defaults to the location the -# :ref:`run-coverage` template gets rendered to if configured. -# Default: .mxmake/files/run-coverage.sh -COVERAGE_COMMAND?=.mxmake/files/run-coverage.sh - -## qa.mypy - -# Source folder for code analysis. -# Default: src -MYPY_SRC?=src - -# Mypy Python requirements to be installed (via pip). -# Default: types-setuptools -MYPY_REQUIREMENTS?=types-setuptools types-docutils types-PyYAML - ## core.help # Request to show all targets, descriptions and arguments for a given domain. @@ -383,45 +369,6 @@ FORMAT_TARGETS+=ruff-format DIRTY_TARGETS+=ruff-dirty CLEAN_TARGETS+=ruff-clean -############################################################################## -# isort -############################################################################## - -# Adjust ISORT_SRC to respect PROJECT_PATH_PYTHON if still at default -ifeq ($(ISORT_SRC),src) -ISORT_SRC:=$(PYTHON_PROJECT_PREFIX)src -endif - -ISORT_TARGET:=$(SENTINEL_FOLDER)/isort.sentinel -$(ISORT_TARGET): $(MXENV_TARGET) - @echo "Install isort" - @$(PYTHON_PACKAGE_COMMAND) install isort - @touch $(ISORT_TARGET) - -.PHONY: isort-check -isort-check: $(ISORT_TARGET) - @echo "Run isort check" - @isort --check $(ISORT_SRC) - -.PHONY: isort-format -isort-format: $(ISORT_TARGET) - @echo "Run isort format" - @isort $(ISORT_SRC) - -.PHONY: isort-dirty -isort-dirty: - @rm -f $(ISORT_TARGET) - -.PHONY: isort-clean -isort-clean: isort-dirty - @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y isort || : - -INSTALL_TARGETS+=$(ISORT_TARGET) -CHECK_TARGETS+=isort-check -FORMAT_TARGETS+=isort-format -DIRTY_TARGETS+=isort-dirty -CLEAN_TARGETS+=isort-clean - ############################################################################## # sphinx ############################################################################## @@ -567,6 +514,47 @@ INSTALL_TARGETS+=packages DIRTY_TARGETS+=packages-dirty CLEAN_TARGETS+=packages-clean +############################################################################## +# ty +############################################################################## + +# Adjust TY_SRC to respect PROJECT_PATH_PYTHON if still at default +ifeq ($(TY_SRC),src) +TY_SRC:=$(PYTHON_PROJECT_PREFIX)src +endif + +# Build ty flags +TY_FLAGS:= +ifneq ($(TY_PYTHON_VERSION),) +TY_FLAGS+=--python-version $(TY_PYTHON_VERSION) +endif + +TY_TARGET:=$(SENTINEL_FOLDER)/ty.sentinel +$(TY_TARGET): $(MXENV_TARGET) + @echo "Install ty" + @$(PYTHON_PACKAGE_COMMAND) install ty + @touch $(TY_TARGET) + +.PHONY: ty +ty: $(PACKAGES_TARGET) $(TY_TARGET) + @echo "Run ty" + @ty check $(TY_FLAGS) $(TY_SRC) + +.PHONY: ty-dirty +ty-dirty: + @rm -f $(TY_TARGET) + +.PHONY: ty-clean +ty-clean: ty-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y ty || : + @rm -rf .ty + +INSTALL_TARGETS+=$(TY_TARGET) +CHECK_TARGETS+=ty +TYPECHECK_TARGETS+=ty +CLEAN_TARGETS+=ty-clean +DIRTY_TARGETS+=ty-dirty + ############################################################################## # test ############################################################################## @@ -596,69 +584,6 @@ INSTALL_TARGETS+=$(TEST_TARGET) CLEAN_TARGETS+=test-clean DIRTY_TARGETS+=test-dirty -############################################################################## -# coverage -############################################################################## - -COVERAGE_TARGET:=$(SENTINEL_FOLDER)/coverage.sentinel -$(COVERAGE_TARGET): $(TEST_TARGET) - @echo "Install Coverage" - @$(PYTHON_PACKAGE_COMMAND) install -U coverage - @touch $(COVERAGE_TARGET) - -.PHONY: coverage -coverage: $(FILES_TARGET) $(SOURCES_TARGET) $(PACKAGES_TARGET) $(COVERAGE_TARGET) - @test -z "$(COVERAGE_COMMAND)" && echo "No coverage command defined" && exit 1 || : - @echo "Run coverage using $(COVERAGE_COMMAND)" - @/usr/bin/env bash -c "$(COVERAGE_COMMAND)" - -.PHONY: coverage-dirty -coverage-dirty: - @rm -f $(COVERAGE_TARGET) - -.PHONY: coverage-clean -coverage-clean: coverage-dirty - @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y coverage || : - @rm -rf .coverage htmlcov - -INSTALL_TARGETS+=$(COVERAGE_TARGET) -DIRTY_TARGETS+=coverage-dirty -CLEAN_TARGETS+=coverage-clean - -############################################################################## -# mypy -############################################################################## - -# Adjust MYPY_SRC to respect PROJECT_PATH_PYTHON if still at default -ifeq ($(MYPY_SRC),src) -MYPY_SRC:=$(PYTHON_PROJECT_PREFIX)src -endif - -MYPY_TARGET:=$(SENTINEL_FOLDER)/mypy.sentinel -$(MYPY_TARGET): $(MXENV_TARGET) - @echo "Install mypy" - @$(PYTHON_PACKAGE_COMMAND) install mypy $(MYPY_REQUIREMENTS) - @touch $(MYPY_TARGET) - -.PHONY: mypy -mypy: $(PACKAGES_TARGET) $(MYPY_TARGET) - @echo "Run mypy" - @mypy $(MYPY_SRC) - -.PHONY: mypy-dirty -mypy-dirty: - @rm -f $(MYPY_TARGET) - -.PHONY: mypy-clean -mypy-clean: mypy-dirty - @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y mypy || : - @rm -rf .mypy_cache - -INSTALL_TARGETS+=$(MYPY_TARGET) -TYPECHECK_TARGETS+=mypy -CLEAN_TARGETS+=mypy-clean -DIRTY_TARGETS+=mypy-dirty - ############################################################################## # help ############################################################################## diff --git a/src/mxmake/main.py b/src/mxmake/main.py index 2ef31b4f..7112c4bf 100644 --- a/src/mxmake/main.py +++ b/src/mxmake/main.py @@ -67,6 +67,8 @@ def list_command(args: argparse.Namespace): sys.stdout.write(f"Requested domain not found: {args.domain}\n") sys.exit(1) + assert domain is not None # type narrowing for ty + sys.stdout.write(f"Domain {topic.name}.{domain.name}:\n") depends = ", ".join(domain.depends) if domain.depends else "No dependencies" sys.stdout.write(f" Depends: {depends}\n") diff --git a/src/mxmake/templates.py b/src/mxmake/templates.py index ab39f360..b83b84f8 100644 --- a/src/mxmake/templates.py +++ b/src/mxmake/templates.py @@ -58,19 +58,23 @@ def __init__( # XXX: if environment is None, default to ``get_template_environment``? self.environment = environment - @abc.abstractproperty + @property + @abc.abstractmethod def target_folder(self) -> Path: """Target folder for rendered template.""" - @abc.abstractproperty + @property + @abc.abstractmethod def target_name(self) -> str: """Target file name for rendered template.""" - @abc.abstractproperty + @property + @abc.abstractmethod def template_name(self) -> str: """Template name to use.""" - @abc.abstractproperty + @property + @abc.abstractmethod def template_variables(self) -> dict[str, typing.Any]: """Variables for template rendering.""" diff --git a/src/mxmake/testing/__init__.py b/src/mxmake/testing/__init__.py index 7e14d5ff..0cb753bc 100644 --- a/src/mxmake/testing/__init__.py +++ b/src/mxmake/testing/__init__.py @@ -73,10 +73,6 @@ def __init__( class RenderTestCase(unittest.TestCase): - class Example: - def __init__(self, want): - self.want = want + "\n" - class Failure(Exception): pass @@ -96,6 +92,8 @@ def checkOutput(self, want, got, optionflags=None): if not success: raise RenderTestCase.Failure( self._checker.output_difference( - RenderTestCase.Example(want), got, optionflags + doctest.Example(source="", want=want + "\n"), + got, + optionflags, ) ) diff --git a/src/mxmake/tests/test_templates.py b/src/mxmake/tests/test_templates.py index 8751b45a..be752330 100644 --- a/src/mxmake/tests/test_templates.py +++ b/src/mxmake/tests/test_templates.py @@ -52,7 +52,7 @@ class Template(templates.Template): def test_Template(self, tempdir: Path): # cannot instantiate abstract template with self.assertRaises(TypeError): - templates.Template() # type: ignore + templates.Template() # create test template class Template(templates.Template): diff --git a/src/mxmake/tests/test_topics.py b/src/mxmake/tests/test_topics.py index b95e26e9..92e6d2f1 100644 --- a/src/mxmake/tests/test_topics.py +++ b/src/mxmake/tests/test_topics.py @@ -3,6 +3,7 @@ from dataclasses import field from mxmake import testing from mxmake import topics +from pathlib import Path import configparser import typing @@ -57,10 +58,13 @@ @dataclass class _TestDomain(topics.Domain): + file: str | Path = field(default=Path()) depends_: list[str] = field(default_factory=list) soft_depends_: list[str] = field(default_factory=list) def __post_init__(self) -> None: + if isinstance(self.file, str): + self.file = Path(self.file) self.runtime_depends = self.depends + self.soft_depends @property @@ -167,7 +171,9 @@ def test_Topic(self, tmpdir): self.assertEqual(topic_domains[1].name, "domain-b") self.assertEqual(topic_domains[1].topic, "topic") - self.assertEqual(topic.domain("domain-a").name, "domain-a") + domain_a = topic.domain("domain-a") + assert domain_a is not None + self.assertEqual(domain_a.name, "domain-a") self.assertEqual(topic.domain("inexistent"), None) def test_DomainConflictError(self): @@ -182,7 +188,7 @@ def test_CircularDependencyDomainError(self): str(err), ( "Domains define circular dependencies: [_TestDomain(" - "topic='t1', name='f1', file='f1.mk', depends_=['f2'], soft_depends_=[])]" + f"topic='t1', name='f1', file={Path('f1.mk')!r}, depends_=['f2'], soft_depends_=[])]" ), ) @@ -193,7 +199,7 @@ def test_MissingDependencyDomainError(self): str(err), ( "Domain define missing dependency: _TestDomain(" - "topic='t', name='t', file='t.mk', depends_=['missing'], soft_depends_=[])" + f"topic='t', name='t', file={Path('t.mk')!r}, depends_=['missing'], soft_depends_=[])" ), ) diff --git a/src/mxmake/topics.py b/src/mxmake/topics.py index 85dd7125..fb8e27b1 100644 --- a/src/mxmake/topics.py +++ b/src/mxmake/topics.py @@ -165,7 +165,7 @@ def domain(self, name: str) -> Domain | None: @functools.lru_cache(maxsize=4096) def load_topics() -> list[Topic]: - return [ep.load() for ep in load_eps_by_group("mxmake.topics")] # type: ignore + return [ep.load() for ep in load_eps_by_group("mxmake.topics")] def get_topic(name: str) -> Topic: diff --git a/src/mxmake/topics/qa/ty.mk b/src/mxmake/topics/qa/ty.mk new file mode 100644 index 00000000..c6346722 --- /dev/null +++ b/src/mxmake/topics/qa/ty.mk @@ -0,0 +1,63 @@ +#:[ty] +#:title = ty +#:description = Static type checking with ty (Astral's fast type checker). +#:depends = core.packages +#: +#:[target.ty] +#:description = Run ty type checker. +#: +#:[setting.TY_SRC] +#:description = Source folder for type checking. +#:default = src +#: +#:[setting.TY_PYTHON_VERSION] +#:description = Target Python version for type checking (e.g., 3.12). +#: Leave empty to use default detection. +#:default = +#: +#:[target.ty-dirty] +#:description = Marks ty dirty. +#: +#:[target.ty-clean] +#:description = Uninstall ty and removes cached data. + +############################################################################## +# ty +############################################################################## + +# Adjust TY_SRC to respect PROJECT_PATH_PYTHON if still at default +ifeq ($(TY_SRC),src) +TY_SRC:=$(PYTHON_PROJECT_PREFIX)src +endif + +# Build ty flags +TY_FLAGS:= +ifneq ($(TY_PYTHON_VERSION),) +TY_FLAGS+=--python-version $(TY_PYTHON_VERSION) +endif + +TY_TARGET:=$(SENTINEL_FOLDER)/ty.sentinel +$(TY_TARGET): $(MXENV_TARGET) + @echo "Install ty" + @$(PYTHON_PACKAGE_COMMAND) install ty + @touch $(TY_TARGET) + +.PHONY: ty +ty: $(PACKAGES_TARGET) $(TY_TARGET) + @echo "Run ty" + @ty check $(TY_FLAGS) $(TY_SRC) + +.PHONY: ty-dirty +ty-dirty: + @rm -f $(TY_TARGET) + +.PHONY: ty-clean +ty-clean: ty-dirty + @test -e $(MXENV_PYTHON) && $(MXENV_PYTHON) -m pip uninstall -y ty || : + @rm -rf .ty + +INSTALL_TARGETS+=$(TY_TARGET) +CHECK_TARGETS+=ty +TYPECHECK_TARGETS+=ty +CLEAN_TARGETS+=ty-clean +DIRTY_TARGETS+=ty-dirty diff --git a/src/mxmake/utils.py b/src/mxmake/utils.py index 8eaf305b..778d4d57 100644 --- a/src/mxmake/utils.py +++ b/src/mxmake/utils.py @@ -1,7 +1,6 @@ from pathlib import Path import os -import typing NAMESPACE = "mxmake-" @@ -27,7 +26,7 @@ def ns_name(name: str) -> str: return f"{NAMESPACE}{name}" -def list_value(value: str) -> list[str]: +def list_value(value: str | None) -> list[str]: """Convert string value from config file to list of strings. Separator is space. Supports newline. """