diff --git a/COVERAGE.md b/COVERAGE.md index b289b6e..5af6cf2 100644 --- a/COVERAGE.md +++ b/COVERAGE.md @@ -1,10 +1,18 @@ -| Name | Stmts | Miss | Branch | BrPart | Cover | -|---------------------------------- | -------: | -------: | -------: | -------: | ------: | -| src/hatch\_cython/\_\_init\_\_.py | 2 | 0 | 0 | 0 | 100% | -| src/hatch\_cython/config.py | 282 | 19 | 124 | 12 | 92% | -| src/hatch\_cython/devel.py | 5 | 0 | 0 | 0 | 100% | -| src/hatch\_cython/hooks.py | 5 | 1 | 2 | 0 | 86% | -| src/hatch\_cython/plugin.py | 194 | 9 | 102 | 8 | 94% | -| src/hatch\_cython/types.py | 19 | 5 | 2 | 1 | 71% | -| src/hatch\_cython/utils.py | 10 | 0 | 2 | 0 | 100% | -| **TOTAL** | **517** | **34** | **232** | **21** | **92%** | +| Name | Stmts | Miss | Branch | BrPart | Cover | +|----------------------------------------- | -------: | -------: | -------: | -------: | ------: | +| src/hatch\_cython/\_\_init\_\_.py | 2 | 0 | 0 | 0 | 100% | +| src/hatch\_cython/config/\_\_init\_\_.py | 2 | 0 | 0 | 0 | 100% | +| src/hatch\_cython/config/autoimport.py | 9 | 0 | 4 | 0 | 100% | +| src/hatch\_cython/config/config.py | 145 | 16 | 70 | 10 | 87% | +| src/hatch\_cython/config/defaults.py | 6 | 0 | 0 | 0 | 100% | +| src/hatch\_cython/config/files.py | 22 | 1 | 12 | 2 | 91% | +| src/hatch\_cython/config/flags.py | 53 | 1 | 16 | 0 | 99% | +| src/hatch\_cython/config/platform.py | 71 | 2 | 28 | 2 | 96% | +| src/hatch\_cython/constants.py | 11 | 0 | 0 | 0 | 100% | +| src/hatch\_cython/devel.py | 5 | 0 | 0 | 0 | 100% | +| src/hatch\_cython/hooks.py | 5 | 1 | 2 | 0 | 86% | +| src/hatch\_cython/plugin.py | 189 | 9 | 142 | 8 | 95% | +| src/hatch\_cython/temp.py | 12 | 0 | 2 | 0 | 100% | +| src/hatch\_cython/types.py | 19 | 4 | 2 | 1 | 76% | +| src/hatch\_cython/utils.py | 22 | 0 | 8 | 0 | 100% | +| **TOTAL** | **573** | **34** | **286** | **23** | **93%** | diff --git a/README.md b/README.md index dd21ef5..1024c2d 100644 --- a/README.md +++ b/README.md @@ -87,11 +87,12 @@ include_somelib = { pkg = "pyarrow", include="get_include", libraries="get_libra | extra_link_args | str or `{ platforms = ["*"] \| "*", arg = str }` | | env | `{ env = "VAR1", arg = "VALUE", platforms = ["*"], arch = ["*"] }`
 if flag is one of:
 - ARFLAGS
 - LDSHARED 
 - LDFLAGS
 - CPPFLAGS 
 - CFLAGS 
 - CCSHARED
the current env vars will be merged with the value (provided platform & arch applies), separated by a space. This can be enabled by adding `{ env = "MYVAR" ... , merges = true }` to the definition. | | includes | list str | -| includes\_{package} | `{ pkg = str, include = str, libraries = str\| None, library_dirs = str \| None , required_call = str \| None }` 
 where all fields, but `pkg`, are attributes of `pkg` in the type of `callable() -> list[str] \| str` \| `list[str] \| str`. `pkg` is a module, or loadable module object, which may be imported through `import x.y.z`. | -| includes_numpy \| includes_pyarrow | bool
 3rd party named imports. must have the respective opt in `dependencies` | -| retain_intermediate_artifacts | bool = False 
 whether to keep `.c` \| `.cpp` files | -| parallel | bool = False 
if parallel, add openmp headers
 important: if using macos, you need the *homebrew* llvm vs _apple's_ llvm in order to pass `-fopenmp` to clang compiler | -| compiler | compiler used at build-time. if `msvc` (Microsoft Visual Studio), `/openmp` is used as argument to compile instead of `-fopenmp`  when `parallel = true` | +| includes\_{package} | `{ pkg = str, include = str, libraries = str\| None, library_dirs = str \| None , required_call = str \| None }` 
where all fields, but `pkg`, are attributes of `pkg` in the type of `callable() -> list[str] \| str` \| `list[str] \| str`. `pkg` is a module, or loadable module object, which may be imported through `import x.y.z`. | +| includes_numpy \| includes_pyarrow | bool
3rd party named imports. must have the respective opt in `dependencies` | +| retain_intermediate_artifacts | bool = False 
whether to keep `.c` \| `.cpp` files | +| parallel | bool = False 
if parallel, add openmp headers
important: if using macos, you need the *homebrew* llvm vs _apple's_ llvm in order to pass `-fopenmp` to clang compiler | +| compiler | compiler used at build-time. if `msvc` (Microsoft Visual Studio), `/openmp` is used as argument to compile instead of `-fopenmp`  when `parallel = true`. `default = false` | +| compile_py | whether to include `.py` files when building cython exts. note, this can be enabled & you can do per file / matched file ignores as below. `default = true` | | \*\* kwargs | keyword = value pair arguments to pass to the extension module when building | ### Files @@ -100,12 +101,15 @@ include_somelib = { pkg = "pyarrow", include="get_include", libraries="get_libra [build.targets.wheel.hooks.cython.options.files] exclude = [ # anything matching no_compile is ignored by cython - "*/no_compile/*" + "*/no_compile/*", + # note - anything "*" is escaped to "([^\s]*)" (non whitespace). + # if you need an actual * for python regex, use as below: + # this excludes all pyd or pytempl extensions + "([^.]\\*).(pyd$|pytempl$)" ] +aliases = {"abclib._filewithoutsuffix" = "abclib.importalias"} ``` - - ## License `hatch-cython` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. diff --git a/example/hatch.toml b/example/hatch.toml index d1e92db..b8ccf7c 100644 --- a/example/hatch.toml +++ b/example/hatch.toml @@ -24,7 +24,6 @@ src = "example_lib" parallel = true include_numpy = true retain_intermediate_artifacts = true - directives = { language_level = 3, boundscheck = false } compile_args = [ { arg = "-v" }, @@ -51,6 +50,7 @@ cythonize_kwargs = { annotate = true, nthreads = 4 } exclude = [ "*/no_compile/*" ] +aliases = {"example_lib._alias" = "example_lib.aliased"} [[envs.all.matrix]] python = ["3.8", "3.9", "3.10", "3.11"] diff --git a/example/src/example_lib/_alias.pyx b/example/src/example_lib/_alias.pyx new file mode 100644 index 0000000..34392e6 --- /dev/null +++ b/example/src/example_lib/_alias.pyx @@ -0,0 +1,4 @@ +# distutils: language=c++ + +cpdef str some_aliased(str name): + return name diff --git a/example/src/example_lib/mod_a/deep_nest/creates.pyx b/example/src/example_lib/mod_a/deep_nest/creates.pyx new file mode 100644 index 0000000..cb880a5 --- /dev/null +++ b/example/src/example_lib/mod_a/deep_nest/creates.pyx @@ -0,0 +1,9 @@ +cdef class MyClass: + def __cinit__(self): + pass + + cpdef str do(self): + return "abc" + +cpdef MyClass fast_create(): + return MyClass.__new__(MyClass) diff --git a/example/tests/test_aliased.py b/example/tests/test_aliased.py new file mode 100644 index 0000000..0205da7 --- /dev/null +++ b/example/tests/test_aliased.py @@ -0,0 +1,4 @@ +def test_aliased(): + from example_lib.aliased import some_aliased + + assert some_aliased("abc") == "abc" diff --git a/example/tests/test_submodule.py b/example/tests/test_submodule.py index c530a89..1a24be4 100644 --- a/example/tests/test_submodule.py +++ b/example/tests/test_submodule.py @@ -1,14 +1,21 @@ -from example_lib.mod_a.adds import fmul, imul -from example_lib.mod_a.some_defn import ValueDefn - - def test_muls(): + from example_lib.mod_a.adds import fmul, imul + assert fmul(5.5, 5.5) == 30.25 assert imul(21, 2) == 42 def test_vals(): + from example_lib.mod_a.some_defn import ValueDefn + v = ValueDefn(10) assert v.value == 10 v.set(5) assert v.value == 5 + + +def test_deep_nesting(): + from example_lib.mod_a.deep_nest.creates import fast_create + + o = fast_create() + assert o.do() == "abc" diff --git a/src/hatch_cython/__about__.py b/src/hatch_cython/__about__.py index 664c19b..af7d6ac 100644 --- a/src/hatch_cython/__about__.py +++ b/src/hatch_cython/__about__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: 2023-present joshua-auchincloss # # SPDX-License-Identifier: MIT -__version__ = "0.2.3" +__version__ = "0.2.4" diff --git a/src/hatch_cython/config/__init__.py b/src/hatch_cython/config/__init__.py new file mode 100644 index 0000000..0875259 --- /dev/null +++ b/src/hatch_cython/config/__init__.py @@ -0,0 +1,2 @@ +from hatch_cython.config.config import Config, parse_from_dict +from hatch_cython.config.platform import PlatformArgs diff --git a/src/hatch_cython/config/autoimport.py b/src/hatch_cython/config/autoimport.py new file mode 100644 index 0000000..8321c8c --- /dev/null +++ b/src/hatch_cython/config/autoimport.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass, field + + +@dataclass +class Autoimport: + pkg: str + + include: str + libraries: str = field(default=None) + library_dirs: str = field(default=None) + required_call: str = field(default=None) + + +__packages__ = { + a.pkg: a + for a in ( + Autoimport("numpy", "get_include"), + Autoimport( + "pyarrow", + include="get_include", + libraries="get_libraries", + library_dirs="get_library_dirs", + required_call="create_library_symlinks", + ), + ) +} diff --git a/src/hatch_cython/config.py b/src/hatch_cython/config/config.py similarity index 52% rename from src/hatch_cython/config.py rename to src/hatch_cython/config/config.py index 45cab67..0b41a4a 100644 --- a/src/hatch_cython/config.py +++ b/src/hatch_cython/config/config.py @@ -1,46 +1,26 @@ -import platform -from collections.abc import Generator, Hashable +from collections.abc import Generator from dataclasses import asdict, dataclass, field from importlib import import_module -from os import environ, path -from typing import ClassVar, Optional +from os import path +from typing import Optional from hatch.utils.ci import running_in_ci from hatchling.builders.hooks.plugin.interface import BuildHookInterface -from packaging.markers import Marker - -from hatch_cython.types import CorePlatforms, ListStr, callable_t, dict_t, list_t, union_t -from hatch_cython.utils import memo - -EXIST_TRIM = 2 - -ANON = "anon" -INCLUDE = "include_" -OPTIMIZE = "-O2" -DIRECTIVES = { - "binding": True, - "language_level": 3, -} -LTPY311 = "python_version < '3.11'" -MUST_UNIQUE = ["-O", "-arch", "-march"] -POSIX_CORE: list_t[CorePlatforms] = ["darwin", "linux"] - - -@memo -def plat(): - return platform.system().lower() - - -@memo -def aarch(): - return platform.machine().lower() +from hatch_cython.config.autoimport import Autoimport, __packages__ +from hatch_cython.config.defaults import get_default_compile, get_default_link +from hatch_cython.config.files import FileArgs +from hatch_cython.config.flags import EnvFlag, EnvFlags, parse_env_args +from hatch_cython.config.platform import ListedArgs, PlatformArgs, parse_platform_args +from hatch_cython.constants import DIRECTIVES, EXIST_TRIM, INCLUDE, LTPY311, MUST_UNIQUE +from hatch_cython.types import CallableT, ListStr # fields tracked by this plugin __known__ = ( "src", "env", "files", + "compile_py", "includes", "libraries", "library_dirs", @@ -52,224 +32,6 @@ def aarch(): ) -@dataclass -class Autoimport: - pkg: str - - include: str - libraries: str = field(default=None) - library_dirs: str = field(default=None) - required_call: str = field(default=None) - - -__packages__ = { - a.pkg: a - for a in ( - Autoimport("numpy", "get_include"), - Autoimport( - "pyarrow", - include="get_include", - libraries="get_libraries", - library_dirs="get_library_dirs", - required_call="create_library_symlinks", - ), - ) -} - - -@dataclass -class PlatformBase(Hashable): - platforms: union_t[ListStr, str] = "*" - arch: union_t[ListStr, str] = "*" - depends_path: bool = False - marker: str = None - apply_to_marker: callable_t[[], bool] = None - - def __post_init__(self): - self.do_rewrite("platforms") - self.do_rewrite("arch") - - def do_rewrite(self, attr: str): - att = getattr(self, attr) - if isinstance(att, list): - setattr(self, attr, [p.lower() for p in att]) - elif isinstance(att, str): - setattr(self, attr, att.lower()) - - def check_marker(self): - do = True - if self.apply_to_marker: - do = self.apply_to_marker() - if do: - marker = Marker(self.marker) - return marker.evaluate() - return False - - def _applies_impl(self, attr: str, defn: str): - if self.marker: - ok = self.check_marker() - if not ok: - return False - - att = getattr(self, attr) - if isinstance(att, list): - # https://docs.python.org/3/library/platform.html#platform.machine - # "" is a possible value so we have to add conditions for anon - _anon = ANON in att and defn == "" - return defn in att or "*" in att or _anon - _anon = ANON == att and defn == "" - return (att in (defn, "*")) or _anon - - def applies(self): - _isplatform = self._applies_impl("platforms", plat()) - _isarch = self._applies_impl("arch", aarch()) - return _isplatform and _isarch - - def is_exist(self, trim: int = 0): - if self.depends_path: - return path.exists(self.arg[trim:]) - return True - - -@dataclass -class PlatformArgs(PlatformBase): - arg: str = None - - def __hash__(self) -> int: - return hash(self.arg) - - -@dataclass -class EnvFlag(PlatformArgs): - env: str = field(default="") - merges: bool = field(default=False) - - def __hash__(self) -> int: - return hash(self.field) - - -__flags__ = ( - EnvFlag(env="CC", merges=False), - EnvFlag(env="CPP", merges=False), - EnvFlag(env="CXX", merges=False), - EnvFlag(env="CFLAGS", merges=True), - EnvFlag(env="CCSHARED", merges=True), - EnvFlag(env="CPPFLAGS", merges=True), - EnvFlag(env="LDFLAGS", merges=True), - EnvFlag(env="LDSHARED", merges=True), - EnvFlag(env="SHLIB_SUFFIX", merges=False), - EnvFlag(env="AR", merges=False), - EnvFlag(env="ARFLAGS", merges=True), -) - - -@dataclass -class EnvFlags: - CC: PlatformArgs = None - CPP: PlatformArgs = None - CXX: PlatformArgs = None - - CFLAGS: PlatformArgs = None - CCSHARED: PlatformArgs = None - - CPPFLAGS: PlatformArgs = None - - LDFLAGS: PlatformArgs = None - LDSHARED: PlatformArgs = None - - SHLIB_SUFFIX: PlatformArgs = None - - AR: PlatformArgs = None - ARFLAGS: PlatformArgs = None - - custom: dict_t[str, PlatformArgs] = field(default_factory=dict) - env: dict = field(default_factory=environ.copy) - - __known__: ClassVar[dict_t[str, EnvFlag]] = {e.env: e for e in __flags__} - - def __post_init__(self): - for flag in __flags__: - self.merge_to_env(flag, self.get_from_self) - for flag in self.custom.values(): - self.merge_to_env(flag, self.get_from_custom) - - def merge_to_env(self, flag: EnvFlag, get: callable_t[[str], EnvFlag]): - var = environ.get(flag.env) - override: EnvFlag = get(flag.env) - if override and flag.merges: - add = var + " " if var else "" - self.env[flag.env] = add + override.arg - elif override: - self.env[flag.env] = override.arg - - def get_from_self(self, attr): - return getattr(self, attr) - - def get_from_custom(self, attr): - return self.custom.get(attr) - - -ListedArgs = list_t[union_t[PlatformArgs, str]] -""" -List[str | PlatformArgs] -""" - - -@dataclass -class FileArgs: - exclude: ListStr = field(default_factory=list) - - -def get_default_link(): - return [ - PlatformArgs(arg="-L/opt/homebrew/lib", platforms=POSIX_CORE, depends_path=True), - PlatformArgs(arg="-L/usr/local/lib", platforms=POSIX_CORE, depends_path=True), - PlatformArgs(arg="-L/usr/local/opt", platforms=POSIX_CORE, depends_path=True), - ] - - -def get_default_compile(): - return [ - PlatformArgs(arg="-O2"), - PlatformArgs(arg="-I/opt/homebrew/include", platforms=POSIX_CORE, depends_path=True), - PlatformArgs(arg="-I/usr/local/include", platforms=POSIX_CORE, depends_path=True), - ] - - -def parse_to_plat(cls, arg, args: union_t[list, dict], key: union_t[int, str], require_argform: bool, **kwargs): - if isinstance(arg, dict): - args[key] = cls(**arg, **kwargs) - elif require_argform: - msg = f"arg {key} is invalid. must be of type ({{ flag = ... , platform = '*' }}) given {arg} ({type(arg)})" - raise ValueError(msg) - - -def parse_platform_args( - kwargs: dict, - name: str, - default: callable_t[[], list_t[PlatformArgs]], -) -> list_t[union_t[str, PlatformArgs]]: - try: - args = [*default(), *kwargs.pop(name)] - for i, arg in enumerate(args): - parse_to_plat(PlatformArgs, arg, args, i, require_argform=False) - except KeyError: - args = default() - return args - - -def parse_env_args( - kwargs: dict, -): - try: - args: list = kwargs.pop("env") - for i, arg in enumerate(args): - parse_to_plat(EnvFlag, arg, args, i, require_argform=True) - except KeyError: - args = [] - return args - - def parse_from_dict(cls: BuildHookInterface): given = cls.config.get("options", {}) passed = given.copy() @@ -359,6 +121,7 @@ class Config: extra_link_args: ListedArgs = field(default_factory=get_default_link) retain_intermediate_artifacts: bool = field(default=False) envflags: EnvFlags = field(default_factory=EnvFlags) + compile_py: bool = field(default=True) def __post_init__(self): self.directives = {**DIRECTIVES, **self.directives} @@ -377,8 +140,8 @@ def _post_import_attr( im: Autoimport, att: str, mod: any, - extend: callable_t[[ListStr], None], - append: callable_t[[str], None], + extend: CallableT[[ListStr], None], + append: CallableT[[str], None], ): attr = getattr(im, att) if attr is not None: diff --git a/src/hatch_cython/config/defaults.py b/src/hatch_cython/config/defaults.py new file mode 100644 index 0000000..b455ea4 --- /dev/null +++ b/src/hatch_cython/config/defaults.py @@ -0,0 +1,18 @@ +from hatch_cython.config.platform import PlatformArgs +from hatch_cython.constants import POSIX_CORE + + +def get_default_link(): + return [ + PlatformArgs(arg="-L/opt/homebrew/lib", platforms=POSIX_CORE, depends_path=True), + PlatformArgs(arg="-L/usr/local/lib", platforms=POSIX_CORE, depends_path=True), + PlatformArgs(arg="-L/usr/local/opt", platforms=POSIX_CORE, depends_path=True), + ] + + +def get_default_compile(): + return [ + PlatformArgs(arg="-O2"), + PlatformArgs(arg="-I/opt/homebrew/include", platforms=POSIX_CORE, depends_path=True), + PlatformArgs(arg="-I/usr/local/include", platforms=POSIX_CORE, depends_path=True), + ] diff --git a/src/hatch_cython/config/files.py b/src/hatch_cython/config/files.py new file mode 100644 index 0000000..2bddca8 --- /dev/null +++ b/src/hatch_cython/config/files.py @@ -0,0 +1,27 @@ +import re +from dataclasses import dataclass, field + +from hatch_cython.types import ListStr, UnionT, dict_t +from hatch_cython.utils import parse_user_glob + + +@dataclass +class FileArgs: + exclude: ListStr = field(default_factory=list) + aliases: dict_t[str, str] = field(default_factory=dict) + + def __post_init__(self): + rep = {} + for k, v in self.aliases.items(): + rep[parse_user_glob(k)] = v + self.aliases = rep + + def matches_alias(self, other: str) -> UnionT[str, None]: + matched = [re.match(v, other) for v in self.aliases.keys()] + if any(matched): + first = 0 + for ok in matched: + if ok: + break + first += 1 + return self.aliases[list(self.aliases.keys())[first]] diff --git a/src/hatch_cython/config/flags.py b/src/hatch_cython/config/flags.py new file mode 100644 index 0000000..d5a1541 --- /dev/null +++ b/src/hatch_cython/config/flags.py @@ -0,0 +1,88 @@ +from dataclasses import dataclass, field +from os import environ +from typing import ClassVar + +from hatch_cython.config.platform import PlatformArgs, parse_to_plat +from hatch_cython.types import CallableT, dict_t + + +@dataclass +class EnvFlag(PlatformArgs): + env: str = field(default="") + merges: bool = field(default=False) + + def __hash__(self) -> int: + return hash(self.field) + + +__flags__ = ( + EnvFlag(env="CC", merges=False), + EnvFlag(env="CPP", merges=False), + EnvFlag(env="CXX", merges=False), + EnvFlag(env="CFLAGS", merges=True), + EnvFlag(env="CCSHARED", merges=True), + EnvFlag(env="CPPFLAGS", merges=True), + EnvFlag(env="LDFLAGS", merges=True), + EnvFlag(env="LDSHARED", merges=True), + EnvFlag(env="SHLIB_SUFFIX", merges=False), + EnvFlag(env="AR", merges=False), + EnvFlag(env="ARFLAGS", merges=True), +) + + +@dataclass +class EnvFlags: + CC: PlatformArgs = None + CPP: PlatformArgs = None + CXX: PlatformArgs = None + + CFLAGS: PlatformArgs = None + CCSHARED: PlatformArgs = None + + CPPFLAGS: PlatformArgs = None + + LDFLAGS: PlatformArgs = None + LDSHARED: PlatformArgs = None + + SHLIB_SUFFIX: PlatformArgs = None + + AR: PlatformArgs = None + ARFLAGS: PlatformArgs = None + + custom: dict_t[str, PlatformArgs] = field(default_factory=dict) + env: dict = field(default_factory=environ.copy) + + __known__: ClassVar[dict_t[str, EnvFlag]] = {e.env: e for e in __flags__} + + def __post_init__(self): + for flag in __flags__: + self.merge_to_env(flag, self.get_from_self) + for flag in self.custom.values(): + self.merge_to_env(flag, self.get_from_custom) + + def merge_to_env(self, flag: EnvFlag, get: CallableT[[str], EnvFlag]): + var = environ.get(flag.env) + override: EnvFlag = get(flag.env) + if override and flag.merges: + add = var + " " if var else "" + self.env[flag.env] = add + override.arg + elif override: + self.env[flag.env] = override.arg + + def get_from_self(self, attr): + return getattr(self, attr) + + def get_from_custom(self, attr): + return self.custom.get(attr) + + +def parse_env_args( + kwargs: dict, +): + try: + args: list = kwargs.pop("env") + for i, arg in enumerate(args): + parse_to_plat(EnvFlag, arg, args, i, require_argform=True) + except KeyError: + args = [] + return args diff --git a/src/hatch_cython/config/platform.py b/src/hatch_cython/config/platform.py new file mode 100644 index 0000000..b011c1f --- /dev/null +++ b/src/hatch_cython/config/platform.py @@ -0,0 +1,99 @@ +from collections.abc import Hashable +from dataclasses import dataclass +from os import path + +from packaging.markers import Marker + +from hatch_cython.constants import ANON +from hatch_cython.types import CallableT, ListStr, UnionT, list_t +from hatch_cython.utils import aarch, plat + + +@dataclass +class PlatformBase(Hashable): + platforms: UnionT[ListStr, str] = "*" + arch: UnionT[ListStr, str] = "*" + depends_path: bool = False + marker: str = None + apply_to_marker: CallableT[[], bool] = None + + def __post_init__(self): + self.do_rewrite("platforms") + self.do_rewrite("arch") + + def do_rewrite(self, attr: str): + att = getattr(self, attr) + if isinstance(att, list): + setattr(self, attr, [p.lower() for p in att]) + elif isinstance(att, str): + setattr(self, attr, att.lower()) + + def check_marker(self): + do = True + if self.apply_to_marker: + do = self.apply_to_marker() + if do: + marker = Marker(self.marker) + return marker.evaluate() + return False + + def _applies_impl(self, attr: str, defn: str): + if self.marker: + ok = self.check_marker() + if not ok: + return False + + att = getattr(self, attr) + if isinstance(att, list): + # https://docs.python.org/3/library/platform.html#platform.machine + # "" is a possible value so we have to add conditions for anon + _anon = ANON in att and defn == "" + return defn in att or "*" in att or _anon + _anon = ANON == att and defn == "" + return (att in (defn, "*")) or _anon + + def applies(self): + _isplatform = self._applies_impl("platforms", plat()) + _isarch = self._applies_impl("arch", aarch()) + return _isplatform and _isarch + + def is_exist(self, trim: int = 0): + if self.depends_path: + return path.exists(self.arg[trim:]) + return True + + +@dataclass +class PlatformArgs(PlatformBase): + arg: str = None + + def __hash__(self) -> int: + return hash(self.arg) + + +def parse_to_plat(cls, arg, args: UnionT[list, dict], key: UnionT[int, str], require_argform: bool, **kwargs): + if isinstance(arg, dict): + args[key] = cls(**arg, **kwargs) + elif require_argform: + msg = f"arg {key} is invalid. must be of type ({{ flag = ... , platform = '*' }}) given {arg} ({type(arg)})" + raise ValueError(msg) + + +def parse_platform_args( + kwargs: dict, + name: str, + default: CallableT[[], list_t[PlatformArgs]], +) -> list_t[UnionT[str, PlatformArgs]]: + try: + args = [*default(), *kwargs.pop(name)] + for i, arg in enumerate(args): + parse_to_plat(PlatformArgs, arg, args, i, require_argform=False) + except KeyError: + args = default() + return args + + +ListedArgs = list_t[UnionT[PlatformArgs, str]] +""" +List[str | PlatformArgs] +""" diff --git a/src/hatch_cython/constants.py b/src/hatch_cython/constants.py new file mode 100644 index 0000000..8ca8430 --- /dev/null +++ b/src/hatch_cython/constants.py @@ -0,0 +1,15 @@ +from hatch_cython.types import CorePlatforms, list_t + +NORM_GLOB = r"([^\s]*)" +UAST = "${U_AST}" +EXIST_TRIM = 2 +ANON = "anon" +INCLUDE = "include_" +OPTIMIZE = "-O2" +DIRECTIVES = { + "binding": True, + "language_level": 3, +} +LTPY311 = "python_version < '3.11'" +MUST_UNIQUE = ["-O", "-arch", "-march"] +POSIX_CORE: list_t[CorePlatforms] = ["darwin", "linux"] diff --git a/src/hatch_cython/plugin.py b/src/hatch_cython/plugin.py index 540f7d1..8100f80 100644 --- a/src/hatch_cython/plugin.py +++ b/src/hatch_cython/plugin.py @@ -9,71 +9,16 @@ from hatchling.builders.hooks.plugin.interface import BuildHookInterface -from hatch_cython.config import Config, parse_from_dict, plat +from hatch_cython.config import parse_from_dict +from hatch_cython.temp import ExtensionArg, setup_py from hatch_cython.types import ListStr, P, list_t - - -def options_kws(kwds: dict): - return ",\n\t".join((f"{k}={v!r}" for k, v in kwds.items())) - - -def setup_py( - *files: list_t[ListStr], - options: Config, -): - code = """ -from setuptools import Extension, setup -from Cython.Build import cythonize - -COMPILEARGS = {compile_args} -DIRECTIVES = {directives} -INCLUDES = {includes} -LIBRARIES = {libs} -LIBRARY_DIRS = {lib_dirs} -EXTENSIONS = [{ext_files}] -LINKARGS = {extra_link_args} - -if __name__ == "__main__": - exts = [ - Extension("*", - ex, - extra_compile_args=COMPILEARGS, - extra_link_args=LINKARGS, - include_dirs=INCLUDES, - libraries=LIBRARIES, - library_dirs=LIBRARY_DIRS, - {keywords} - ) for ex in EXTENSIONS - ] - ext_modules = cythonize( - exts, - compiler_directives=DIRECTIVES, - include_path=INCLUDES, - {cython} - ) - setup(ext_modules=ext_modules) -""" - ext_files = ",".join([repr(lf) for lf in files]) - kwds = options_kws(options.compile_kwargs) - cython = options_kws(options.cythonize_kwargs) - return code.format( - compile_args=repr(options.compile_args_for_platform), - extra_link_args=repr(options.compile_links_for_platform), - directives=repr(options.directives), - ext_files=ext_files, - keywords=kwds, - cython=cython, - includes=repr(options.includes), - libs=repr(options.libraries), - lib_dirs=repr(options.library_dirs), - ).strip() +from hatch_cython.utils import memo, parse_user_glob, plat class CythonBuildHook(BuildHookInterface): PLUGIN_NAME = "cython" precompiled_extension: ClassVar[list] = [ - ".py", ".pyx", ".pxd", ] @@ -89,27 +34,11 @@ class CythonBuildHook(BuildHookInterface): ".pyd", ] - _config: Config - _dir: str - _included: ListStr - _artifact_patterns: ListStr - _artifact_globs: ListStr - _norm_included_files: ListStr - _norm_artifact_patterns: ListStr - _grouped_norm: list_t[ListStr] - def __init__(self, *args: P.args, **kwargs: P.kwargs): super().__init__(*args, **kwargs) - self._included = None - self._norm_included_files = None - self._artifact_patterns = None - self._artifact_globs = None - self._norm_artifact_patterns = None - self._config = None - self._dir = None - self._grouped_norm = None @property + @memo def is_src(self): return os.path.exists(os.path.join(self.root, "src")) @@ -126,100 +55,110 @@ def normalize_glob(self, pattern: str): return pattern.replace("\\", "/") @property + @memo def dir_name(self): return self.options.src if self.options.src is not None else self.metadata.name @property + @memo def project_dir(self): - if self._dir is None: - if self.is_src: - src = f"./src/{self.dir_name}" - else: - src = f"./{self.dir_name}" - self._dir = src - return self._dir + if self.is_src: + src = f"./src/{self.dir_name}" + else: + src = f"./{self.dir_name}" + return src @property def precompiled_globs(self): _globs = [] for ex in self.precompiled_extension: _globs.extend((f"{self.project_dir}/*{ex}", f"{self.project_dir}/**/*{ex}")) - return _globs + return list(set(_globs)) @property + @memo + def options_exclude(self): + return [parse_user_glob(e) for e in self.options.files.exclude] + + def filter_ensure_wanted(self, tgts: ListStr): + matched = list( + filter( + lambda s: not any(re.match(e, self.normalize_glob(s), re.IGNORECASE) for e in self.options_exclude), + tgts, + ) + ) + return matched + + @property + @memo def included_files(self): - if self._included is None: - self._included = [] - for patt in self.precompiled_globs: - globbed = glob(patt) - matched = list( - filter( - lambda s: not any( - re.match( - self.normalize_glob(e).replace("*", r"([^\s]*)"), self.normalize_glob(s), re.IGNORECASE - ) - for e in self.options.files.exclude - ), - globbed, - ) - ) - self._included.extend(matched) - return self._included + included = [] + _normu = [self.normalize_glob(parse_user_glob(e)) for e in self.options.files.exclude] + self.app.display_debug("user globs") + self.app.display_debug(_normu) + for patt in self.precompiled_globs: + globbed = glob(patt, recursive=True) + if len(globbed) == 0: + continue + matched = self.filter_ensure_wanted(globbed) + included.extend(matched) + return included @property + @memo def normalized_included_files(self): """ Produces files in posix format """ - if self._norm_included_files is None: - self._norm_included_files = [self.normalize_glob(f) for f in self.included_files] - return self._norm_included_files + return [self.normalize_glob(f) for f in self.included_files] @property - def grouped_included_files(self) -> list_t[ListStr]: - if self._grouped_norm is None: - grouped = {} - for norm in self.normalized_included_files: - root, ext = os.path.splitext(norm) - ok = True - if ext == ".pxd": - pyfile = norm.replace(".pxd", ".py") - if os.path.exists(pyfile): - norm = pyfile # noqa: PLW2901 - else: - ok = False - self.app.display_warning(f"attempted to use .pxd file without .py file ({norm})") - if grouped.get(root) and ok: - grouped[root].append(norm) - elif ok: - grouped[root] = [norm] - self._grouped_norm = list(grouped.values()) - return self._grouped_norm + @memo + def grouped_included_files(self) -> list_t[ExtensionArg]: + grouped = {} + for norm in self.normalized_included_files: + root, ext = os.path.splitext(norm) + ok = True + if ext == ".pxd": + pyfile = norm.replace(".pxd", ".py") + if os.path.exists(pyfile): + norm = pyfile # noqa: PLW2901 + else: + ok = False + self.app.display_warning(f"attempted to use .pxd file without .py file ({norm})") + if self.is_src: + root = root.replace("./src/", "") + root = root.replace("/", ".") + alias = self.options.files.matches_alias(root) + if alias: + root = alias + if grouped.get(root) and ok: + grouped[root].append(norm) + elif ok: + grouped[root] = [norm] + return [ExtensionArg(name=key, files=files) for key, files in grouped.items()] @property + @memo def artifact_globs(self): - if self._artifact_globs is None: - artifact_globs = [] - for included_file in self.normalized_included_files: - root, _ = os.path.splitext(included_file) - artifact_globs.extend(f"{root}.*{ext}" for ext in self.precompiled_extension) - self._artifact_globs = artifact_globs - return self._artifact_globs + artifact_globs = [] + for included_file in self.normalized_included_files: + root, _ = os.path.splitext(included_file) + artifact_globs.extend(f"{root}.*{ext}" for ext in self.precompiled_extension) + return artifact_globs @property + @memo def normalized_artifact_globs(self): """ Produces files in platform native format (e.g. a/b vs a\\b) """ - if self._norm_artifact_patterns is None: - self._norm_artifact_patterns = [self.normalize_glob(f) for f in self.artifact_globs] - return self._norm_artifact_patterns + return [self.normalize_glob(f) for f in self.artifact_globs] @property + @memo def artifact_patterns(self): - if self._artifact_patterns is None: - self._artifact_patterns = [f"/{artifact_glob}" for artifact_glob in self.normalized_artifact_globs] - return self._artifact_patterns + return [f"/{artifact_glob}" for artifact_glob in self.normalized_artifact_globs] @contextmanager def get_build_dirs(self): @@ -233,7 +172,7 @@ def _globs(self, exts: ListStr): ] globbed = [] for g in globs: - globbed += [self.normalize_path(f) for f in glob(g)] + globbed += [self.normalize_path(f) for f in glob(g, recursive=True)] return list(set(globbed)) @property @@ -266,10 +205,12 @@ def clean(self, _versions: ListStr): self.clean_compiled() @property + @memo def options(self): - if self._config is None: - self._config = parse_from_dict(self) - return self._config + config = parse_from_dict(self) + if config.compile_py: + self.precompiled_extension.append(".py") + return config def initialize(self, version: str, build_data: dict): self.app.display_mini_header(self.PLUGIN_NAME) @@ -321,7 +262,7 @@ def initialize(self, version: str, build_data: dict): self.app.display_info(process.stdout.decode("utf-8")) self.app.display_success("Post-build artifacts") - self.app.display_info(glob(f"{self.project_dir}/*/**")) + self.app.display_info(glob(f"{self.project_dir}/*/**", recursive=True)) if not self.options.retain_intermediate_artifacts: self.clean_intermediate() diff --git a/src/hatch_cython/temp.py b/src/hatch_cython/temp.py new file mode 100644 index 0000000..09f6944 --- /dev/null +++ b/src/hatch_cython/temp.py @@ -0,0 +1,62 @@ +from typing import TypedDict + +from hatch_cython.config import Config +from hatch_cython.types import ListStr, list_t +from hatch_cython.utils import options_kws + + +class ExtensionArg(TypedDict): + name: str + files: ListStr + + +def setup_py( + *files: list_t[ListStr], + options: Config, +): + code = """ +from setuptools import Extension, setup +from Cython.Build import cythonize + +COMPILEARGS = {compile_args} +DIRECTIVES = {directives} +INCLUDES = {includes} +LIBRARIES = {libs} +LIBRARY_DIRS = {lib_dirs} +EXTENSIONS = [{ext_files}] +LINKARGS = {extra_link_args} + +if __name__ == "__main__": + exts = [ + Extension( ex.get("name"), + ex.get("files"), + extra_compile_args=COMPILEARGS, + extra_link_args=LINKARGS, + include_dirs=INCLUDES, + libraries=LIBRARIES, + library_dirs=LIBRARY_DIRS, + {keywords} + ) for ex in EXTENSIONS + ] + ext_modules = cythonize( + exts, + compiler_directives=DIRECTIVES, + include_path=INCLUDES, + {cython} + ) + setup(ext_modules=ext_modules) +""" + ext_files = ",".join([repr(lf) for lf in files]) + kwds = options_kws(options.compile_kwargs) + cython = options_kws(options.cythonize_kwargs) + return code.format( + compile_args=repr(options.compile_args_for_platform), + extra_link_args=repr(options.compile_links_for_platform), + directives=repr(options.directives), + ext_files=ext_files, + keywords=kwds, + cython=cython, + includes=repr(options.includes), + libs=repr(options.libraries), + lib_dirs=repr(options.library_dirs), + ).strip() diff --git a/src/hatch_cython/types.py b/src/hatch_cython/types.py index 62ed357..0e09367 100644 --- a/src/hatch_cython/types.py +++ b/src/hatch_cython/types.py @@ -18,14 +18,14 @@ dict_t = Dict # noqa: UP006 list_t = List # noqa: UP006 - ListStr = List[str] # noqa: UP006 P = ParamSpec("P") -union_t = Union +ListStr = list_t[str] +UnionT = Union CorePlatforms = Literal[ "darwin", "linux", "windows", ] -callable_t = Callable +CallableT = Callable diff --git a/src/hatch_cython/utils.py b/src/hatch_cython/utils.py index b833f70..386b180 100644 --- a/src/hatch_cython/utils.py +++ b/src/hatch_cython/utils.py @@ -1,11 +1,14 @@ -from hatch_cython.types import P, T, callable_t +import platform +from hatch_cython.constants import NORM_GLOB, UAST +from hatch_cython.types import CallableT, P, T -def memo(func: callable_t[P, T]) -> T: + +def memo(func: CallableT[P, T]) -> CallableT[P, T]: value = None ran = False - def wrapped(*args, **kwargs): + def wrapped(*args: P.args, **kwargs: P.kwargs) -> T: nonlocal value, ran if not ran: value = func(*args, **kwargs) @@ -13,3 +16,21 @@ def wrapped(*args, **kwargs): return value return wrapped + + +@memo +def plat(): + return platform.system().lower() + + +@memo +def aarch(): + return platform.machine().lower() + + +def options_kws(kwds: dict): + return ",\n\t".join((f"{k}={v!r}" for k, v in kwds.items())) + + +def parse_user_glob(uglob: str): + return uglob.replace("\\*", UAST).replace("*", NORM_GLOB).replace(UAST, "*") diff --git a/taskfile.yaml b/taskfile.yaml index 4e497e4..6c6934b 100644 --- a/taskfile.yaml +++ b/taskfile.yaml @@ -18,10 +18,12 @@ tasks: - rm -rf .coverag* - rm -rf __pycache__ - rm -rf **/__pycache__ - - rm -f **/*.c - - rm -f **/**/*.so - - rm -f **/**/*.cpp - - rm -f **/*.so + - rm -rf ./example/src/**/**/*.c + - rm -rf ./example/src/**/**/*.so + - rm -rf ./example/src/**/**/*.cpp + - rm -rf ./example/src/**/**/*.html + - rm -rf ./**/*.so + - rm -rf ./**/*.html - rm -rf .ruff_cache - rm -rf **/.ruff_cache - rm -rf .pytest_cache diff --git a/tests/test_plugin.py b/tests/test_plugin.py index cdb627f..5504e83 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -73,58 +73,95 @@ def test_build_hook(new_proj): with override_dir(new_proj): assert sorted(hook.normalized_included_files) == sorted( [ - "./src/example_lib/normal.py", - "./src/example_lib/__init__.py", "./src/example_lib/__about__.py", + "./src/example_lib/__about__.py", + "./src/example_lib/__init__.py", + "./src/example_lib/__init__.py", + "./src/example_lib/_alias.pyx", + "./src/example_lib/_alias.pyx", "./src/example_lib/mod_a/__init__.py", - "./src/example_lib/mod_a/some_defn.py", - "./src/example_lib/test.pyx", "./src/example_lib/mod_a/adds.pyx", + "./src/example_lib/mod_a/deep_nest/creates.pyx", "./src/example_lib/mod_a/some_defn.pxd", + "./src/example_lib/mod_a/some_defn.py", + "./src/example_lib/normal.py", + "./src/example_lib/normal.py", + "./src/example_lib/test.pyx", + "./src/example_lib/test.pyx", ] ) - assert sorted([sorted(ls) for ls in hook.grouped_included_files]) == sorted( - [ - sorted(ls) - for ls in [ - ["./src/example_lib/normal.py"], - ["./src/example_lib/__init__.py"], - ["./src/example_lib/__about__.py"], - ["./src/example_lib/mod_a/__init__.py"], - ["./src/example_lib/mod_a/some_defn.py", "./src/example_lib/mod_a/some_defn.py"], - ["./src/example_lib/test.pyx"], - ["./src/example_lib/mod_a/adds.pyx"], - ] - ] - ) + assert sorted( + [{**ls, "files": sorted(ls.get("files"))} for ls in hook.grouped_included_files], + key=lambda x: x.get("name"), + ) == [ + { + "name": "example_lib.__about__", + "files": ["./src/example_lib/__about__.py", "./src/example_lib/__about__.py"], + }, + { + "name": "example_lib.__init__", + "files": ["./src/example_lib/__init__.py", "./src/example_lib/__init__.py"], + }, + {"name": "example_lib.aliased", "files": ["./src/example_lib/_alias.pyx", "./src/example_lib/_alias.pyx"]}, + {"name": "example_lib.mod_a.__init__", "files": ["./src/example_lib/mod_a/__init__.py"]}, + {"name": "example_lib.mod_a.adds", "files": ["./src/example_lib/mod_a/adds.pyx"]}, + {"name": "example_lib.mod_a.deep_nest.creates", "files": ["./src/example_lib/mod_a/deep_nest/creates.pyx"]}, + { + "name": "example_lib.mod_a.some_defn", + "files": ["./src/example_lib/mod_a/some_defn.py", "./src/example_lib/mod_a/some_defn.py"], + }, + {"name": "example_lib.normal", "files": ["./src/example_lib/normal.py", "./src/example_lib/normal.py"]}, + {"name": "example_lib.test", "files": ["./src/example_lib/test.pyx", "./src/example_lib/test.pyx"]}, + ] rf = sorted( [ - "./src/example_lib/normal.*.py", - "./src/example_lib/normal.*.pyx", - "./src/example_lib/normal.*.pxd", - "./src/example_lib/__init__.*.py", - "./src/example_lib/__init__.*.pyx", - "./src/example_lib/__init__.*.pxd", + "./src/example_lib/__about__.*.pxd", + "./src/example_lib/__about__.*.pxd", + "./src/example_lib/__about__.*.py", "./src/example_lib/__about__.*.py", "./src/example_lib/__about__.*.pyx", - "./src/example_lib/__about__.*.pxd", + "./src/example_lib/__about__.*.pyx", + "./src/example_lib/__init__.*.pxd", + "./src/example_lib/__init__.*.pxd", + "./src/example_lib/__init__.*.py", + "./src/example_lib/__init__.*.py", + "./src/example_lib/__init__.*.pyx", + "./src/example_lib/__init__.*.pyx", + "./src/example_lib/_alias.*.pxd", + "./src/example_lib/_alias.*.pxd", + "./src/example_lib/_alias.*.py", + "./src/example_lib/_alias.*.py", + "./src/example_lib/_alias.*.pyx", + "./src/example_lib/_alias.*.pyx", + "./src/example_lib/mod_a/__init__.*.pxd", "./src/example_lib/mod_a/__init__.*.py", "./src/example_lib/mod_a/__init__.*.pyx", - "./src/example_lib/mod_a/__init__.*.pxd", - "./src/example_lib/mod_a/some_defn.*.py", - "./src/example_lib/mod_a/some_defn.*.pyx", - "./src/example_lib/mod_a/some_defn.*.pxd", - "./src/example_lib/test.*.py", - "./src/example_lib/test.*.pyx", - "./src/example_lib/test.*.pxd", + "./src/example_lib/mod_a/adds.*.pxd", "./src/example_lib/mod_a/adds.*.py", "./src/example_lib/mod_a/adds.*.pyx", - "./src/example_lib/mod_a/adds.*.pxd", + "./src/example_lib/mod_a/deep_nest/creates.*.pxd", + "./src/example_lib/mod_a/deep_nest/creates.*.py", + "./src/example_lib/mod_a/deep_nest/creates.*.pyx", + "./src/example_lib/mod_a/some_defn.*.pxd", + "./src/example_lib/mod_a/some_defn.*.pxd", + "./src/example_lib/mod_a/some_defn.*.py", "./src/example_lib/mod_a/some_defn.*.py", "./src/example_lib/mod_a/some_defn.*.pyx", - "./src/example_lib/mod_a/some_defn.*.pxd", + "./src/example_lib/mod_a/some_defn.*.pyx", + "./src/example_lib/normal.*.pxd", + "./src/example_lib/normal.*.pxd", + "./src/example_lib/normal.*.py", + "./src/example_lib/normal.*.py", + "./src/example_lib/normal.*.pyx", + "./src/example_lib/normal.*.pyx", + "./src/example_lib/test.*.pxd", + "./src/example_lib/test.*.pxd", + "./src/example_lib/test.*.py", + "./src/example_lib/test.*.py", + "./src/example_lib/test.*.pyx", + "./src/example_lib/test.*.pyx", ] ) assert sorted(hook.normalized_artifact_globs) == rf @@ -142,4 +179,4 @@ def test_build_hook(new_proj): assert build_data.get("infer_tag") assert not build_data.get("pure_python") assert sorted(build_data.get("artifacts")) == sorted([f"/{f}" for f in rf]) - assert len(build_data.get("force_include")) == 7 + assert len(build_data.get("force_include")) == 9 diff --git a/tests/test_setuppy.py b/tests/test_setuppy.py index 246a824..b874418 100644 --- a/tests/test_setuppy.py +++ b/tests/test_setuppy.py @@ -20,13 +20,13 @@ def clean(s: str): INCLUDES = ['/123'] LIBRARIES = ['/abc'] LIBRARY_DIRS = ['/def'] -EXTENSIONS = [['./abc/def.pyx'],['./abc/depb.py']] +EXTENSIONS = [{'name': 'abc.def', 'files': ['./abc/def.pyx']},{'name': 'abc.depb', 'files': ['./abc/depb.py']}] LINKARGS = ['-I/etc/abc/linka.h'] if __name__ == "__main__": exts = [ - Extension("*", - ex, + Extension( ex.get("name"), + ex.get("files"), extra_compile_args=COMPILEARGS, extra_link_args=LINKARGS, include_dirs=INCLUDES, @@ -53,11 +53,11 @@ def test_setup_py(): cythonize_kwargs={"abc": "def"}, extra_link_args=[PlatformArgs(arg="-I/etc/abc/linka.h")], ) - with patch("hatch_cython.config.path.exists", true_if_eq()): + with patch("hatch_cython.config.config.path.exists", true_if_eq()): with arch_platform("x86_64", ""): setup = setup_py( - ["./abc/def.pyx"], - ["./abc/depb.py"], + {"name": "abc.def", "files": ["./abc/def.pyx"]}, + {"name": "abc.depb", "files": ["./abc/depb.py"]}, options=cfg, ) assert clean(setup) == clean(EXPECT) @@ -71,10 +71,10 @@ def test_solo_ext_type_validations(): cythonize_kwargs={"abc": "def"}, extra_link_args=[PlatformArgs(arg="-I/etc/abc/linka.h")], ) - with patch("hatch_cython.config.path.exists", true_if_eq()): + with patch("hatch_cython.config.config.path.exists", true_if_eq()): with arch_platform("x86_64", ""): setup = setup_py( - ["./abc/def.pyx"], + {"name": "abc.def", "files": ["./abc/def.pyx"]}, options=cfg, ) tested = False @@ -85,8 +85,9 @@ def test_solo_ext_type_validations(): ext = ast.literal_eval(ln.replace(exteq, "").strip()) assert isinstance(ext, list) for ex in ext: - ok = [isinstance(node, str) for node in ex] - assert all(ok) + assert isinstance(ex, dict) + assert isinstance(ex.get("name"), str) + assert isinstance(ex.get("files"), list) if not tested: raise ValueError(setup, tested, "missed test") diff --git a/tests/utils.py b/tests/utils.py index 82d8295..c1e7f8b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -29,7 +29,7 @@ def patch_path(arch: str, *extra: str): def wrap(path): return h(path, *extra) - with patch("hatch_cython.config.path.exists", wrap): + with patch("hatch_cython.config.config.path.exists", wrap): yield @@ -42,10 +42,13 @@ def platformgetter(): return platform try: - with patch("hatch_cython.config.aarch", aarchgetter): - with patch("hatch_cython.config.plat", platformgetter): - with patch("hatch_cython.plugin.plat", platformgetter): - yield + with patch("hatch_cython.utils.plat", platformgetter): + with patch("hatch_cython.utils.plat", platformgetter): + with patch("hatch_cython.config.platform.plat", platformgetter): + with patch("hatch_cython.plugin.plat", platformgetter): + with patch("hatch_cython.utils.aarch", aarchgetter): + with patch("hatch_cython.config.platform.aarch", aarchgetter): + yield finally: print(f"Clean {arch}-{platform}") # noqa: T201 del aarchgetter, platformgetter @@ -74,7 +77,7 @@ def get_import(name: str): try: with patch( - "hatch_cython.config.import_module", + "hatch_cython.config.config.import_module", get_import, ): yield @@ -101,7 +104,11 @@ def override_env(d: dict): for k, v in d.items(): new[k] = v os.environ.update(new) - yield + with patch( + "hatch_cython.config.flags.environ", + new, + ): + yield finally: for k, v in current.items(): os.environ[k] = v