Skip to content

Commit 824b91a

Browse files
committed
Runtime and structural changes to compile key modules to C with mypyc
The bulk of the mypyc port: replace custom metaclasses with __init_subclass__ plus explicitly written-out resource members, migrate from the home-grown cached_property to functools.cached_property, add mypyc_attr directives, native-class type-narrowing asserts/casts, split PackageOrderList out into package_order_list.py, and assorted runtime fixes. Pure type-annotation changes are split out into separate commits (the package-resource annotations are folded into "Add generics for package resources"; the rest into "Add type annotations to assorted modules").
1 parent a309625 commit 824b91a

39 files changed

Lines changed: 2177 additions & 601 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ docs/source/commands/
2323
__tests_pkg_repo/
2424
.idea/
2525
venv/
26+
/out*
27+
/build*

mypyc-custom.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
#pip install ../mypy
6+
pip uninstall -y rez
7+
pip install --no-build-isolation -e .
8+
python -m rez.cli._main selftest --debug

mypyc-test.sh

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
builddir=./build
6+
7+
rm -rf $builddir
8+
_REZ_NO_KILLPG=1 python ./install.py -v $builddir
9+
10+
11+
#$builddir/bin/rez/rez-selftest
12+
13+
#_REZ_NO_KILLPG=1 ./build/bin/python -c "import rez.utils.resources; print(rez.utils.resources.__file__); "
14+
#_REZ_NO_KILLPG=1 ./build/bin/python -c "import rez.solver; print(rez.solver.__file__)"
15+
#_REZ_NO_KILLPG=1 ./build/bin/python -c "import rez.resolver; print(rez.resolver.__file__)"
16+
#_REZ_NO_KILLPG=1 ./build/bin/python -c "import rez.packages; print(rez.packages.__file__)"
17+
# ./build/bin/python -c "import rez.version._version; print(rez.version._version.__file__)"
18+
19+
./build/bin/rez/rez-selftest
20+
21+
# ./build/bin/rez/rez-benchmark --out ./out-solver-resolver
22+
23+
# PYTHONPATH=src python -c "import rez.utils.data_utils;rez.utils.data_utils.write_all_dynamic_members()"
24+
25+
# python -m rez.cli._main selftest

pyproject.toml

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,82 @@
11
[build-system]
2-
requires = ["setuptools"]
2+
requires = ["setuptools", "mypy[mypyc]"]
33
build-backend = "setuptools.build_meta"
44

55
[tool.mypy]
66
python_version = "3.8"
77
files = ["src/rez/", "src/rezplugins/"]
88
exclude = [
99
'.*/rez/data/.*',
10-
'.*/rez/vendor/.*',
1110
'.*/rez/tests/.*',
1211
'.*/rez/utils/lint_helper.py',
1312
]
1413
disable_error_code = ["var-annotated", "import-not-found"]
1514
check_untyped_defs = true
1615
# allow this for now:
17-
allow_redefinition = true
16+
#allow_redefinition = true
1817
follow_imports = "silent"
18+
enable_incomplete_feature = ["InlineTypedDict"]
19+
20+
[[tool.mypy.overrides]]
21+
module = "rez.vendor.*"
22+
ignore_errors = true
23+
24+
[[tool.mypy.overrides]]
25+
module = "rez.vendor.schema.schema"
26+
ignore_errors = false
27+
28+
[[tool.mypy.overrides]]
29+
module = "rez.utils.formatting"
30+
disallow_untyped_defs = true
1931

2032
[[tool.mypy.overrides]]
2133
module = 'rez.utils.lint_helper'
2234
follow_imports = "skip"
2335

2436
[[tool.mypy.overrides]]
25-
module = "rez.version._version"
37+
module = "rez.packages"
38+
disallow_untyped_defs = true
39+
40+
[[tool.mypy.overrides]]
41+
module = "rez.package_filter"
42+
disallow_untyped_defs = true
43+
44+
[[tool.mypy.overrides]]
45+
module = "rez.package_order"
46+
disallow_untyped_defs = true
47+
48+
[[tool.mypy.overrides]]
49+
module = "rez.package_repository"
50+
disallow_untyped_defs = true
51+
52+
[[tool.mypy.overrides]]
53+
module = "rez.package_resources"
54+
disallow_untyped_defs = true
55+
56+
[[tool.mypy.overrides]]
57+
module = "rez.solver"
58+
disallow_untyped_defs = true
59+
60+
[[tool.mypy.overrides]]
61+
module = "rez.resolved_context"
62+
disallow_untyped_defs = true
63+
64+
[[tool.mypy.overrides]]
65+
module = "rez.resolver"
2666
disallow_untyped_defs = true
2767

2868
[[tool.mypy.overrides]]
29-
module = "rez.version._requirement"
69+
module = "rez.version.*"
3070
disallow_untyped_defs = true
71+
72+
[[tool.mypy.overrides]]
73+
module = "rez.utils.resources"
74+
disallow_untyped_defs = true
75+
76+
[[tool.mypy.overrides]]
77+
module = "rez.utils.scope"
78+
disallow_untyped_defs = true
79+
80+
[[tool.mypy.overrides]]
81+
module = "rezplugins.package_repository.filesystem"
82+
disallow_untyped_defs = true

setup.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,36 @@ def find_files(pattern, path=None, root="rez"):
4343
long_description = f.read()
4444

4545

46+
if os.environ.get("REZ_MYPYC", "1") != "0":
47+
from mypyc.build import mypycify
48+
ext_modules = mypycify(
49+
[
50+
# "src/rez/package_filter.py", # errors at runtime calling cached_property.uncache: "attribute 'cost' of 'PackageFilter' objects is not writable"
51+
"src/rez/solver.py",
52+
# "src/rez/resolved_context.py", # error in copy(). need to rework from_dict()
53+
"src/rez/resolver.py",
54+
"src/rez/package_order.py", # split out PackageOrderList from this module due to inheriting from list
55+
"src/rez/package_repository.py",
56+
"src/rez/package_resources.py", # requires fixed version of mypyc
57+
"src/rez/version/__init__.py",
58+
"src/rez/version/_util.py",
59+
"src/rez/version/_requirement.py",
60+
"src/rez/version/_version.py",
61+
"src/rez/utils/formatting.py", # includes a mixin, which should not be compiled
62+
# "src/rez/utils/memcached.py",
63+
"src/rez/utils/resources.py",
64+
# "src/rez/utils/scope.py", # objects directly access/modify __dict__
65+
# "src/rez/vendor/schema/schema.py",
66+
"src/rezplugins/package_repository/filesystem.py", # ConfigurationError at runtime with FileSystemPackageRepository plugin
67+
],
68+
# only_compile_paths=["src/rez/solver.py"]
69+
opt_level="3",
70+
multi_file=True
71+
)
72+
else:
73+
ext_modules = []
74+
75+
4676
setup(
4777
name="rez",
4878
version=_rez_version,
@@ -68,7 +98,8 @@ def find_files(pattern, path=None, root="rez"):
6898
packages=find_packages('src', exclude=["build_utils",
6999
"build_utils.*",
70100
"tests"]),
71-
install_requires=[],
101+
install_requires=["mypy_extensions"],
102+
ext_modules=ext_modules,
72103
package_data={
73104
'rez':
74105
['utils/logging.conf'] +

src/rez/cli/env.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def command(opts, parser, extra_arg_groups=None) -> None:
197197
if opts.no_filters:
198198
package_filter = PackageFilterList()
199199
else:
200-
package_filter = PackageFilterList.singleton.copy()
200+
package_filter = PackageFilterList.singleton().copy()
201201

202202
for rule_str in (opts.exclude or []):
203203
rule = Rule.parse_rule(rule_str)

src/rez/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66

77
from rez import __version__
88
from rez.utils.data_utils import AttrDictWrapper, RO_AttrDictWrapper, \
9-
convert_dicts, cached_property, cached_class_property, LazyAttributeMeta, \
9+
convert_dicts, cached_class_property, LazyAttributeMeta, \
1010
deep_update, ModifyList, DelayLoad
11+
from functools import cached_property
1112
from rez.utils.formatting import expandvars, expanduser
1213
from rez.utils.logging_ import get_debug_printer
1314
from rez.utils.scope import scoped_format

src/rez/package_copy.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ def finalize():
241241
variant_resource=src_variant.resource,
242242
overrides=overrides_
243243
)
244+
# not a dry run, so a resource must have been returned
245+
assert dest_variant_resource is not None
244246

245247
dest_variant = Variant(dest_variant_resource)
246248

src/rez/package_filter.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,54 @@
77
from rez.packages import iter_packages, Package
88
from rez.exceptions import ConfigurationError
99
from rez.config import config
10-
from rez.utils.data_utils import cached_property, cached_class_property
10+
from functools import cached_property
1111
from rez.version import VersionedObject, VersionRange, Requirement
1212
from hashlib import sha1
13-
from typing import Any, Iterator, Pattern, TYPE_CHECKING, ClassVar
13+
from typing import cast, Any, Iterator, Pattern, TYPE_CHECKING, ClassVar
1414
import fnmatch
1515
import re
1616

1717
if TYPE_CHECKING:
1818
from typing import Self
1919

20+
import functools
21+
from typing import Callable, Generic, TypeVar
22+
T = TypeVar("T")
23+
TypeT = TypeVar("TypeT", bound=type)
24+
25+
class cached_class_property(Generic[T]):
26+
"""Simple class property caching descriptor.
27+
28+
Example:
29+
30+
>>> class Foo(object):
31+
>>> @cached_class_property
32+
>>> def bah(cls):
33+
>>> print('bah')
34+
>>> return 1
35+
>>>
36+
>>> Foo.bah
37+
bah
38+
1
39+
>>> Foo.bah
40+
1
41+
"""
42+
def __init__(self, func: Callable[[], T], name: str | None = None) -> None:
43+
self.func = func
44+
# Make sure that Sphinx autodoc can follow and get the docstring from our wrapped function.
45+
# TODO: Doesn't work...
46+
functools.update_wrapper(self, func) # type: ignore[arg-type]
47+
48+
def __get__(self, instance: Any, owner: Any | None = None) -> T:
49+
assert owner
50+
name = "_class_property_" + self.func.__name__
51+
result: T | type[KeyError] = getattr(owner, name, KeyError)
52+
53+
if result is KeyError:
54+
result = self.func()
55+
setattr(owner, name, result)
56+
return cast(T, result)
57+
2058

2159
class PackageFilterBase(object):
2260
"""Base class for package filters."""
@@ -58,7 +96,6 @@ def to_pod(self) -> Any:
5896
"""Convert to POD type, suitable for storing in an rxt file.
5997
6098
Return type depends on subclass implementation
61-
dict[str, list[str]]:
6299
"""
63100
raise NotImplementedError
64101

@@ -220,7 +257,8 @@ def _add_rule(self, rules_dict: dict[str | None, list[Rule]], rule: Rule) -> Non
220257
family = rule.family()
221258
rules_ = rules_dict.get(family, [])
222259
rules_dict[family] = sorted(rules_ + [rule], key=lambda x: x.cost())
223-
cached_property.uncache(self, "cost") # type: ignore[attr-defined]
260+
# invalidate the functools.cached_property cache entry
261+
self.__dict__.pop("cost", None)
224262

225263
def __str__(self) -> str:
226264
def sortkey(rule_items: tuple[str | None, list[Rule]]) -> tuple[str, list[Rule]]:
@@ -233,6 +271,15 @@ def sortkey(rule_items: tuple[str | None, list[Rule]]) -> tuple[str, list[Rule]]
233271
sorted(self._includes.items(), key=sortkey)))
234272

235273

274+
def _singleton() -> PackageFilterList:
275+
"""Filter list as configured by :data:`package_filter`.
276+
277+
Returns:
278+
PackageFilterList:
279+
"""
280+
return PackageFilterList.from_pod(config.package_filter)
281+
282+
236283
class PackageFilterList(PackageFilterBase):
237284
"""A list of package filters.
238285
@@ -318,7 +365,7 @@ def __str__(self) -> str:
318365
filters = sorted(self.filters, key=lambda x: (x.cost, str(x)))
319366
return str(tuple(filters))
320367

321-
@cached_class_property
368+
@classmethod
322369
def singleton(cls) -> PackageFilterList:
323370
"""Filter list as configured by :data:`package_filter`.
324371
@@ -327,6 +374,8 @@ def singleton(cls) -> PackageFilterList:
327374
"""
328375
return cls.from_pod(config.package_filter)
329376

377+
# singleton = cached_class_property(_singleton)
378+
330379

331380
#: filter that does not exclude any packages
332381
no_filter = PackageFilterList()

0 commit comments

Comments
 (0)