diff --git a/.github/workflows/benchmark.yaml b/.github/workflows/benchmark.yaml index f66d5a2724..b9c4157e01 100644 --- a/.github/workflows/benchmark.yaml +++ b/.github/workflows/benchmark.yaml @@ -21,7 +21,8 @@ jobs: strategy: matrix: python-version: - - '3.7' + - '3.10' + mypyc: [false, true] steps: - name: Checkout @@ -33,6 +34,8 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install Rez + env: + REZ_MYPYC: ${{ matrix.mypyc && '1' || '0' }} run: | mkdir ./installdir @@ -51,7 +54,7 @@ jobs: - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: "benchmark-result-${{ matrix.python-version }}" + name: "benchmark-result-${{ matrix.python-version }}-mypyc-${{ matrix.mypyc }}" path: ./out store_benchmark_result: @@ -62,7 +65,7 @@ jobs: strategy: matrix: python-version: - - '3.7' + - '3.10' # so we don't have jobs trying to push to git at the same time max-parallel: 1 @@ -70,7 +73,7 @@ jobs: steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: "benchmark-result-${{ matrix.python-version }}" + name: "benchmark-result-${{ matrix.python-version }}-mypyc-false" path: . - name: Checkout (release) diff --git a/.github/workflows/installation.yaml b/.github/workflows/installation.yaml index faec5441db..7e82ab42ca 100644 --- a/.github/workflows/installation.yaml +++ b/.github/workflows/installation.yaml @@ -27,7 +27,7 @@ permissions: jobs: main: - name: ${{ matrix.os }} - ${{ matrix.python-version }} - ${{ matrix.method }} + name: ${{ matrix.os }} - ${{ matrix.python-version }} - ${{ matrix.method }} - mypyc=${{ matrix.mypyc }} runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -35,6 +35,14 @@ jobs: os: ['ubuntu-latest', 'macos-latest', 'windows-latest'] python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] method: ['install' ,'pip'] + mypyc: [false, true] + + exclude: + # mypyc requires Python >= 3.10 (mypy does not support older versions) + - python-version: '3.8' + mypyc: true + - python-version: '3.9' + mypyc: true include: # ubuntu @@ -76,6 +84,7 @@ jobs: - name: Install env: MATRIX_PYTHON_VERSION: ${{ matrix.python-version }} + REZ_MYPYC: ${{ matrix.mypyc && '1' || '0' }} run: | ${{ matrix.REZ_INSTALL_COMMAND }} diff --git a/.github/workflows/mypy.yaml b/.github/workflows/mypy.yaml index 16711bdb0e..fa3d9be563 100644 --- a/.github/workflows/mypy.yaml +++ b/.github/workflows/mypy.yaml @@ -38,7 +38,7 @@ jobs: - name: Install Dependencies run: | - pip install mypy==1.14.1 mypy_baseline==0.7.1 types-setuptools + pip install mypy==2.1.0 mypy_baseline==0.7.1 types-setuptools - name: Run mypy run: >- diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 691b43ce8a..5471b928eb 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -39,6 +39,15 @@ jobs: matrix: os: ['macos-latest', 'ubuntu-latest', 'windows-latest'] python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13'] + mypyc: [false, true] + + exclude: + # mypyc requires Python >= 3.10 (mypy does not support older versions) + - python-version: '3.8' + mypyc: true + - python-version: '3.9' + mypyc: true + include: - os: macos-latest shells: 'sh,csh,bash,tcsh,zsh,pwsh' @@ -73,6 +82,8 @@ jobs: sudo apt-get install -y csh tcsh zsh - name: Install rez + env: + REZ_MYPYC: ${{ matrix.mypyc && '1' || '0' }} run: python ./install.py ./installdir - name: Install test system dependencies (macOS) diff --git a/.gitignore b/.gitignore index 09eebc709a..8a8e3f5888 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,5 @@ docs/source/commands/ __tests_pkg_repo/ .idea/ venv/ +/out* +/build* diff --git a/mypy-baseline.txt b/mypy-baseline.txt index 371a050148..af12127fad 100644 --- a/mypy-baseline.txt +++ b/mypy-baseline.txt @@ -1,107 +1,22 @@ -src/rez/utils/which.py:0: error: Incompatible types in assignment (expression has type "Union[List[str], Any]", variable has type "Optional[str]") [assignment] -src/rez/utils/which.py:0: error: Item "str" of "Union[str, Any]" has no attribute "insert" [union-attr] -src/rez/deprecations.py:0: error: Incompatible types in assignment (expression has type "Callable[[Any, Any, VarArg(Any), KwArg(Any)], str]", variable has type "Callable[[Union[Warning, str], Type[Warning], str, int, Optional[str]], str]") [assignment] -src/rez/version/_version.py:0: error: Unsupported operand types for > ("VersionToken" and "object") [operator] -src/rez/version/_version.py:0: error: Argument 2 to "map" has incompatible type "Optional[List[_SubToken]]"; expected "Iterable[_SubToken]" [arg-type] -src/rez/version/_version.py:0: error: Unsupported operand types for < ("List[_SubToken]" and "None") [operator] -src/rez/version/_version.py:0: error: Unsupported operand types for > ("List[_SubToken]" and "None") [operator] -src/rez/version/_version.py:0: error: Unsupported left operand type for < ("None") [operator] -src/rez/version/_version.py:0: error: Value of type "Optional[List[_SubToken]]" is not indexable [index] -src/rez/version/_version.py:0: error: Incompatible return value type (got "None", expected "Version") [return-value] -src/rez/version/_version.py:0: error: Argument 2 to "map" has incompatible type "Optional[List[VersionToken]]"; expected "Iterable[VersionToken]" [arg-type] -src/rez/version/_version.py:0: error: "object" has no attribute "tokens" [attr-defined] -src/rez/version/_version.py:0: error: "object" has no attribute "tokens" [attr-defined] -src/rez/version/_version.py:0: error: Incompatible types in assignment (expression has type "Version", variable has type "None") [assignment] -src/rez/version/_version.py:0: error: "None" has no attribute "tokens" [attr-defined] -src/rez/version/_version.py:0: error: Incompatible types in assignment (expression has type "_LowerBound", variable has type "None") [assignment] -src/rez/version/_version.py:0: error: Incompatible types in assignment (expression has type "_UpperBound", variable has type "None") [assignment] -src/rez/version/_version.py:0: error: Argument 1 to "_UpperBound" has incompatible type "None"; expected "Version" [arg-type] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "contains_version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "contains_version" [union-attr] -src/rez/version/_version.py:0: error: Unsupported operand types for <= ("_LowerBound" and "None") [operator] -src/rez/version/_version.py:0: error: Unsupported operand types for >= ("_LowerBound" and "None") [operator] -src/rez/version/_version.py:0: error: Unsupported left operand type for <= ("None") [operator] -src/rez/version/_version.py:0: error: Unsupported operand types for >= ("_UpperBound" and "None") [operator] -src/rez/version/_version.py:0: error: Unsupported operand types for <= ("_UpperBound" and "None") [operator] -src/rez/version/_version.py:0: error: Unsupported left operand type for >= ("None") [operator] -src/rez/version/_version.py:0: error: Value of type variable "SupportsRichComparisonT" of "max" cannot be "Optional[_LowerBound]" [type-var] -src/rez/version/_version.py:0: error: Value of type variable "SupportsRichComparisonT" of "min" cannot be "Optional[_UpperBound]" [type-var] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: Value of type variable "SupportsRichComparisonT" of "max" cannot be "Optional[_LowerBound]" [type-var] -src/rez/version/_version.py:0: error: Value of type variable "SupportsRichComparisonT" of "min" cannot be "Optional[_UpperBound]" [type-var] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: Incompatible types in assignment (expression has type "_Bound", variable has type "None") [assignment] -src/rez/version/_version.py:0: error: Argument 1 to "append" of "list" has incompatible type "None"; expected "_Bound" [arg-type] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: "_ContainsVersionIterator" expects no type arguments, but 1 given [type-arg] -src/rez/version/_version.py:0: error: "_ContainsVersionIterator" expects no type arguments, but 1 given [type-arg] -src/rez/version/_version.py:0: error: "_ContainsVersionIterator" expects no type arguments, but 1 given [type-arg] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_Bound]" has no attribute "upper" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Union[_UpperBound, Any, None]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: Value of type variable "SupportsRichComparisonT" of "max" cannot be "Optional[_UpperBound]" [type-var] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_LowerBound]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "version" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_UpperBound]" has no attribute "inclusive" [union-attr] -src/rez/version/_version.py:0: error: "_ContainsVersionIterator" expects no type arguments, but 1 given [type-arg] -src/rez/version/_version.py:0: error: Unsupported operand types for > ("int" and "None") [operator] -src/rez/version/_version.py:0: error: Invalid index type "Optional[int]" for "List[_Bound]"; expected type "SupportsIndex" [index] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_Bound]" has no attribute "upper_bounded" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_Bound]" has no attribute "version_containment" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_Bound]" has no attribute "lower_bounded" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_Bound]" has no attribute "version_containment" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_Bound]" has no attribute "version_containment" [union-attr] -src/rez/version/_version.py:0: error: Item "None" of "Optional[_Bound]" has no attribute "lower_bounded" [union-attr] +src/rez/vendor/schema/schema.py:0: error: Argument 1 to "SchemaError" has incompatible type "list[str]"; expected "str | list[str | None] | None" [arg-type] +src/rez/vendor/schema/schema.py:0: error: Unsupported operand types for + ("list[str]" and "list[str | None]") [operator] +src/rez/vendor/schema/schema.py:0: error: Argument 2 to "SchemaError" has incompatible type "list[str | None]"; expected "str | list[str]" [arg-type] +src/rez/vendor/schema/schema.py:0: error: Argument 1 to "SchemaError" has incompatible type "list[None]"; expected "str | list[str | None] | None" [arg-type] +src/rez/vendor/schema/schema.py:0: error: Unsupported operand types for + ("list[None]" and "list[str | None]") [operator] +src/rez/vendor/schema/schema.py:0: error: Argument 1 to "SchemaError" has incompatible type "list[str]"; expected "str | list[str | None] | None" [arg-type] +src/rez/vendor/schema/schema.py:0: error: Unsupported operand types for + ("list[str]" and "list[str | None]") [operator] +src/rez/vendor/schema/schema.py:0: error: Argument 1 to "SchemaError" has incompatible type "list[None]"; expected "str | list[str | None] | None" [arg-type] +src/rez/vendor/schema/schema.py:0: error: Unsupported operand types for + ("list[None]" and "list[str | None]") [operator] +src/rez/vendor/schema/schema.py:0: error: Argument 1 to "SchemaError" has incompatible type "list[None]"; expected "str | list[str | None] | None" [arg-type] +src/rez/vendor/schema/schema.py:0: error: Unsupported operand types for + ("list[None]" and "list[str | None]") [operator] +src/rez/deprecations.py:0: error: Incompatible types in assignment (expression has type "def formatwarning(_: Any, category: Any, *args: Any, **kwargs: Any) -> str", variable has type "Callable[[Warning | str, type[Warning], str, int, str | None], str]") [assignment] src/rez/utils/py_dist.py:0: error: "Distribution" has no attribute "project_name" [attr-defined] src/rez/utils/py_dist.py:0: error: Argument 1 to "from_name" of "Distribution" has incompatible type "Distribution"; expected "str" [arg-type] src/rez/utils/py_dist.py:0: error: "Distribution" has no attribute "project_name" [attr-defined] -src/rez/utils/py_dist.py:0: error: "List[str]" not callable [operator] +src/rez/utils/py_dist.py:0: error: "list[str]" not callable [operator] src/rez/utils/py_dist.py:0: error: "None" not callable [misc] src/rez/utils/py_dist.py:0: error: "Distribution" has no attribute "project_name" [attr-defined] -src/rez/utils/py_dist.py:0: error: "List[str]" not callable [operator] +src/rez/utils/py_dist.py:0: error: "list[str]" not callable [operator] src/rez/utils/py_dist.py:0: error: "None" not callable [misc] src/rez/utils/py_dist.py:0: error: "Distribution" has no attribute "location" [attr-defined] src/rez/utils/py_dist.py:0: error: "Distribution" has no attribute "location" [attr-defined] @@ -115,387 +30,124 @@ src/rez/utils/py_dist.py:0: error: "Distribution" has no attribute "location" [ src/rez/utils/py_dist.py:0: error: "Distribution" has no attribute "location" [attr-defined] src/rez/utils/py_dist.py:0: error: "Distribution" has no attribute "location" [attr-defined] src/rez/utils/py_dist.py:0: error: "Distribution" has no attribute "location" [attr-defined] -src/rez/version/_requirement.py:0: error: Incompatible return value type (got "Optional[str]", expected "str") [return-value] -src/rez/version/_requirement.py:0: error: Incompatible return value type (got "Optional[Version]", expected "Version") [return-value] -src/rez/version/_requirement.py:0: error: Unsupported left operand type for + ("None") [operator] -src/rez/version/_requirement.py:0: error: Unsupported left operand type for + ("None") [operator] -src/rez/version/_requirement.py:0: error: Incompatible return value type (got "Optional[str]", expected "str") [return-value] -src/rez/version/_requirement.py:0: error: Incompatible return value type (got "Optional[VersionRange]", expected "VersionRange") [return-value] -src/rez/version/_requirement.py:0: error: Item "None" of "Optional[VersionRange]" has no attribute "issuperset" [union-attr] -src/rez/version/_requirement.py:0: error: Argument 1 to "issuperset" of "VersionRange" has incompatible type "Optional[VersionRange]"; expected "VersionRange" [arg-type] -src/rez/version/_requirement.py:0: error: Item "None" of "Optional[VersionRange]" has no attribute "issuperset" [union-attr] -src/rez/version/_requirement.py:0: error: Argument 1 to "issuperset" of "VersionRange" has incompatible type "Optional[VersionRange]"; expected "VersionRange" [arg-type] -src/rez/version/_requirement.py:0: error: Item "None" of "Optional[VersionRange]" has no attribute "intersects" [union-attr] -src/rez/version/_requirement.py:0: error: Argument 1 to "intersects" of "VersionRange" has incompatible type "Optional[VersionRange]"; expected "VersionRange" [arg-type] -src/rez/version/_requirement.py:0: error: Unsupported operand types for in ("Optional[Version]" and "VersionRange") [operator] -src/rez/version/_requirement.py:0: error: Unsupported right operand type for in ("Optional[VersionRange]") [operator] -src/rez/version/_requirement.py:0: error: Unsupported operand types for in ("Optional[Version]" and "VersionRange") [operator] -src/rez/version/_requirement.py:0: error: Unsupported right operand type for in ("Optional[VersionRange]") [operator] -src/rez/version/_requirement.py:0: error: Unsupported operand types for | ("VersionRange" and "None") [operator] -src/rez/version/_requirement.py:0: error: Unsupported left operand type for | ("None") [operator] -src/rez/version/_requirement.py:0: error: Unsupported operand types for - ("VersionRange" and "None") [operator] -src/rez/version/_requirement.py:0: error: Unsupported left operand type for - ("None") [operator] -src/rez/version/_requirement.py:0: error: Unsupported operand types for & ("VersionRange" and "None") [operator] -src/rez/version/_requirement.py:0: error: Unsupported left operand type for & ("None") [operator] -src/rez/version/_requirement.py:0: error: Item "None" of "Optional[VersionRange]" has no attribute "is_any" [union-attr] -src/rez/version/_requirement.py:0: error: Unsupported operand types for + ("str" and "None") [operator] -src/rez/version/_requirement.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "None") [assignment] -src/rez/version/_requirement.py:0: error: Incompatible return value type (got "None", expected "str") [return-value] -src/rez/utils/data_utils.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "Tuple[Any, ...]") [assignment] -src/rez/utils/data_utils.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "Dict[str, Any]") [assignment] -src/rez/utils/data_utils.py:0: error: Unexpected keyword argument "name" for "property" [call-arg] -src/rez/utils/patching.py:0: error: No overload variant of "__setitem__" of "list" matches argument types "int", "None" [call-overload] +src/rez/utils/patching.py:0: error: Name "requires" already defined on line 0 [no-redef] src/rez/utils/scope.py:0: error: Module "typing" has no attribute "Self" [attr-defined] -src/rez/util.py:0: error: Module "typing" has no attribute "TypeGuard" [attr-defined] -src/rez/util.py:0: error: Argument 1 to "module_from_spec" has incompatible type "Optional[ModuleSpec]"; expected "ModuleSpec" [arg-type] -src/rez/util.py:0: error: Item "None" of "Optional[ModuleSpec]" has no attribute "loader" [union-attr] -src/rez/util.py:0: error: Item "None" of "Union[Loader, Any, None]" has no attribute "exec_module" [union-attr] -src/rez/utils/backcompat.py:0: error: Item "None" of "Optional[Match[str]]" has no attribute "groupdict" [union-attr] -src/rez/utils/backcompat.py:0: error: Item "None" of "Optional[Match[str]]" has no attribute "groupdict" [union-attr] -src/rez/__init__.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "Callable[[Any, Any], None]") [assignment] -src/rez/__init__.py:0: error: Function "callback" could always be true in boolean context [truthy-function] -src/rez/utils/sourcecode.py:0: error: Item "None" of "Optional[Callable[[], T]]" has no attribute "__name__" [union-attr] -src/rez/utils/sourcecode.py:0: error: Argument 1 to "getsourcelines" has incompatible type "Optional[Callable[[], T]]"; expected "Union[Module, Type[Any], MethodType, FunctionType, TracebackType, FrameType, CodeType, Callable[..., Any]]" [arg-type] -src/rez/utils/sourcecode.py:0: error: "PackageBaseResourceWrapper" has no attribute "base" [attr-defined] +src/rez/utils/scope.py:0: error: Name "ClassVar" is not defined [name-defined] +src/rez/utils/scope.py:0: error: Name "ClassVar" is not defined [name-defined] +src/rez/utils/scope.py:0: error: Name "NoReturn" is not defined [name-defined] +src/rez/util.py:0: error: Argument 1 to "module_from_spec" has incompatible type "ModuleSpec | None"; expected "ModuleSpec" [arg-type] +src/rez/util.py:0: error: Item "None" of "ModuleSpec | None" has no attribute "loader" [union-attr] +src/rez/util.py:0: error: Item "None" of "Loader | Any | None" has no attribute "exec_module" [union-attr] +src/rez/utils/data_utils.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "tuple[Any, ...]") [assignment] +src/rez/utils/data_utils.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "dict[str, Any]") [assignment] +src/rez/utils/data_utils.py:0: error: Argument 1 of "__contains__" is incompatible with supertype "typing.Mapping"; supertype defines the argument type as "object" [override] +src/rez/utils/data_utils.py:0: error: Argument 1 of "__contains__" is incompatible with supertype "typing.Container"; supertype defines the argument type as "object" [override] +src/rez/utils/data_utils.py:0: error: "object" has no attribute "__iter__"; maybe "__dir__" or "__str__"? (not iterable) [attr-defined] +src/rez/utils/data_utils.py:0: error: Item "None" of "dict[Any, Any] | None" has no attribute "copy" [union-attr] +src/rez/utils/data_utils.py:0: error: Argument 1 to "open" has incompatible type "str | None"; expected "int | str | bytes | PathLike[str] | PathLike[bytes]" [arg-type] +src/rez/utils/data_utils.py:0: error: Argument 1 to "open" has incompatible type "str | None"; expected "int | str | bytes | PathLike[str] | PathLike[bytes]" [arg-type] +src/rez/utils/backcompat.py:0: error: Item "None" of "Match[str] | None" has no attribute "groupdict" [union-attr] +src/rez/utils/backcompat.py:0: error: Item "None" of "Match[str] | None" has no attribute "groupdict" [union-attr] +src/rez/utils/sourcecode.py:0: error: "PackageBaseResourceWrapper[Any]" has no attribute "base" [attr-defined] +src/rez/utils/resources.py:0: error: Module "typing" has no attribute "Self" [attr-defined] src/rez/utils/platform_.py:0: error: Module has no attribute "windll" [attr-defined] src/rez/utils/platform_.py:0: error: Module has no attribute "WinError" [attr-defined] src/rez/utils/platform_.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "Platform") [assignment] -src/rez/utils/filesystem.py:0: error: Argument 2 to "chmod" has incompatible type "Optional[int]"; expected "int" [arg-type] src/rez/utils/filesystem.py:0: error: Module has no attribute "WindowsError" [attr-defined] -src/rez/utils/filesystem.py:0: error: Incompatible types in assignment (expression has type "bytes", variable has type "str") [assignment] -src/rez/utils/filesystem.py:0: error: Argument 1 to "ord" has incompatible type "int"; expected "Union[str, bytes, bytearray]" [arg-type] -src/rez/utils/filesystem.py:0: error: Name "unicode" is not defined [name-defined] -src/rez/utils/filesystem.py:0: error: Name "unicode" is not defined [name-defined] -src/rez/utils/filesystem.py:0: error: Name "xrange" is not defined [name-defined] -src/rez/rex.py:0: error: Item "None" of "Optional[ActionManager]" has no attribute "environ" [union-attr] -src/rez/rex.py:0: error: Signature of "format" incompatible with supertype "Formatter" [override] +src/rez/config.py:0: error: Incompatible return value type (got "MutableMapping[Any, Any]", expected "RO_AttrDictWrapper") [return-value] +src/rez/config.py:0: error: Incompatible types in assignment (expression has type "MutableMapping[Any, Any]", variable has type "dict[str, Any]") [assignment] +src/rez/config.py:0: error: Argument 3 to "convert_dicts" has incompatible type "tuple[type[dict[_KT, _VT]], type[AttrDictWrapper]]"; expected "type[MutableMapping[Any, Any]]" [arg-type] +src/rez/rex.py:0: error: Item "None" of "ActionManager | None" has no attribute "environ" [union-attr] +src/rez/rex.py:0: error: Signature of "format" incompatible with supertype "string.Formatter" [override] +src/rez/plugin_managers.py:0: error: Incompatible types in assignment (expression has type "reversed[str]", variable has type "MutableSequence[str]") [assignment] src/rez/plugin_managers.py:0: error: Too few arguments for "find_spec" of "MetaPathFinderProtocol" [call-arg] -src/rez/plugin_managers.py:0: error: Item "None" of "Optional[ModuleSpec]" has no attribute "loader" [union-attr] -src/rez/plugin_managers.py:0: error: Item "None" of "Union[Loader, Any, None]" has no attribute "load_module" [union-attr] +src/rez/plugin_managers.py:0: error: Item "None" of "ModuleSpec | None" has no attribute "loader" [union-attr] +src/rez/plugin_managers.py:0: error: Item "None" of "Loader | Any | None" has no attribute "load_module" [union-attr] src/rez/plugin_managers.py:0: error: "EntryPoint" has no attribute "__name__" [attr-defined] -src/rez/plugin_managers.py:0: error: Item "MetaPathFinderProtocol" of "Union[MetaPathFinderProtocol, PathEntryFinderProtocol]" has no attribute "path" [union-attr] -src/rez/plugin_managers.py:0: error: Item "PathEntryFinderProtocol" of "Union[MetaPathFinderProtocol, PathEntryFinderProtocol]" has no attribute "path" [union-attr] -src/rez/plugin_managers.py:0: error: Argument 1 to "append" of "list" has incompatible type "Tuple[str, str, Union[Any, str], str]"; expected "List[str]" [arg-type] -src/rez/plugin_managers.py:0: error: Argument 1 to "append" of "list" has incompatible type "Tuple[str, str, str, str]"; expected "List[str]" [arg-type] -src/rez/utils/resources.py:0: error: Module "typing" has no attribute "Self" [attr-defined] -src/rez/utils/resources.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "str") [assignment] -src/rez/utils/memcached.py:0: error: "Callable[[VarArg(Any), KwArg(Any)], Any]" has no attribute "forget" [attr-defined] -src/rez/utils/memcached.py:0: error: "Callable[[VarArg(Any), KwArg(Any)], Any]" has no attribute "__wrapped__" [attr-defined] -src/rez/utils/amqp.py:0: error: Incompatible types in assignment (expression has type "PlainCredentials", target has type "Dict[str, str]") [assignment] +src/rez/plugin_managers.py:0: error: Item "MetaPathFinderProtocol" of "MetaPathFinderProtocol | PathEntryFinderProtocol" has no attribute "path" [union-attr] +src/rez/plugin_managers.py:0: error: Item "PathEntryFinderProtocol" of "MetaPathFinderProtocol | PathEntryFinderProtocol" has no attribute "path" [union-attr] +src/rez/plugin_managers.py:0: error: Incompatible types in assignment (expression has type "RezPluginType", variable has type "str") [assignment] +src/rez/plugin_managers.py:0: error: "str" has no attribute "create_instance" [attr-defined] +src/rez/plugin_managers.py:0: error: Argument 1 to "append" of "list" has incompatible type "tuple[str, str, Any | str, str]"; expected "list[str]" [arg-type] +src/rez/plugin_managers.py:0: error: Argument 1 to "append" of "list" has incompatible type "tuple[str, str, str, str]"; expected "list[str]" [arg-type] +src/rez/utils/amqp.py:0: error: Incompatible types in assignment (expression has type "PlainCredentials", target has type "dict[str, str]") [assignment] src/rez/shells.py:0: error: Module "typing" has no attribute "Self" [attr-defined] src/rez/shells.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "str") [assignment] src/rez/shells.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "str") [assignment] src/rez/shells.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "str") [assignment] src/rez/shells.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "str") [assignment] -src/rez/shells.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "List[str]") [assignment] -src/rez/package_resources.py:0: error: Incompatible types in assignment (expression has type "Type[PackageMetadataError]", base class "Resource" defined the type as "Type[Exception]") [assignment] -src/rez/package_resources.py:0: error: Argument 1 to "get_resource" of "PackageRepository" has incompatible type "None"; expected "Union[str, Type[Resource]]" [arg-type] -src/rez/package_resources.py:0: error: Incompatible types in "yield" (actual type "Resource", expected type "VariantResourceHelper") [misc] -src/rez/package_resources.py:0: error: "PackageRepositoryResource" has no attribute "hashed_variants" [attr-defined] -src/rez/package_resources.py:0: error: "VariantResourceHelper" has no attribute "base" [attr-defined] -src/rez/package_resources.py:0: error: "VariantResourceHelper" has no attribute "base" [attr-defined] -src/rez/package_resources.py:0: error: "VariantResourceHelper" has no attribute "base" [attr-defined] -src/rez/package_resources.py:0: error: "VariantResourceHelper" has no attribute "base" [attr-defined] -src/rez/package_resources.py:0: error: "VariantResourceHelper" has no attribute "base" [attr-defined] -src/rez/package_resources.py:0: error: "VariantResourceHelper" has no attribute "base" [attr-defined] -src/rez/package_resources.py:0: error: Argument 2 to "join" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rez/package_resources.py:0: error: "PackageRepositoryResource" has no attribute "variants" [attr-defined] -src/rez/package_repository.py:0: error: "Type[PackageRepository]" has no attribute "create_repository" [attr-defined] -src/rez/package_repository.py:0: error: "PackageFamilyResource" has no attribute "iter_packages" [attr-defined] -src/rez/package_repository.py:0: error: Argument 1 to "make_resource_handle" of "PackageRepository" has incompatible type "Union[str, Type[Resource]]"; expected "str" [arg-type] -src/rez/package_repository.py:0: error: Incompatible types in assignment (expression has type "Tuple[str, str]", variable has type "List[str]") [assignment] -src/rez/serialise.py:0: error: Incompatible default for argument "filepath" (default has type "None", argument has type "str") [assignment] -src/rez/serialise.py:0: error: Incompatible default for argument "filepath" (default has type "None", argument has type "str") [assignment] -src/rez/serialise.py:0: error: Incompatible types in assignment (expression has type "List[FrameSummary]", variable has type "StackSummary") [assignment] +src/rez/shells.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "list[str]") [assignment] +src/rez/serialise.py:0: error: Incompatible default for parameter "filepath" (default has type "None", parameter has type "str") [assignment] +src/rez/serialise.py:0: error: Incompatible default for parameter "filepath" (default has type "None", parameter has type "str") [assignment] +src/rez/serialise.py:0: error: Incompatible types in assignment (expression has type "list[FrameSummary]", variable has type "StackSummary") [assignment] src/rez/serialise.py:0: error: Unsupported right operand type for in (Module) [operator] src/rez/serialise.py:0: error: Value of type Module is not indexable [index] -src/rez/serialise.py:0: error: Incompatible default for argument "filepath" (default has type "None", argument has type "str") [assignment] -src/rez/serialise.py:0: error: Incompatible default for argument "filepath" (default has type "None", argument has type "str") [assignment] -src/rez/serialise.py:0: error: "Callable[[str, Any, Any], Any]" has no attribute "forget" [attr-defined] -src/rez/packages.py:0: error: Item "None" of "Optional[Dict[str, Any]]" has no attribute "items" [union-attr] -src/rez/packages.py:0: error: Incompatible types in assignment (expression has type "Set[str]", base class "ResourceWrapper" defined the type as "None") [assignment] -src/rez/packages.py:0: error: Argument 1 to "iter_packages" of "PackageRepository" has incompatible type "Resource"; expected "PackageFamilyResource" [arg-type] -src/rez/packages.py:0: error: "Resource" has no attribute "uri" [attr-defined] -src/rez/packages.py:0: error: "Resource" has no attribute "config" [attr-defined] -src/rez/packages.py:0: error: Item "None" of "Optional[Dict[str, Any]]" has no attribute "get" [union-attr] -src/rez/packages.py:0: error: "PackageBaseResourceWrapper" has no attribute "parent" [attr-defined] -src/rez/packages.py:0: error: Incompatible types in assignment (expression has type "Set[str]", base class "ResourceWrapper" defined the type as "None") [assignment] -src/rez/packages.py:0: error: Unsupported right operand type for in ("Optional[Dict[str, Any]]") [operator] -src/rez/packages.py:0: error: Value of type "Optional[Dict[str, Any]]" is not indexable [index] -src/rez/packages.py:0: error: Item "None" of "Optional[Dict[str, Any]]" has no attribute "keys" [union-attr] -src/rez/packages.py:0: error: Argument 1 to "get_parent_package_family" of "PackageRepository" has incompatible type "Resource"; expected "PackageResourceHelper" [arg-type] -src/rez/packages.py:0: error: Item "None" of "Optional[Dict[str, Any]]" has no attribute "get" [union-attr] -src/rez/packages.py:0: error: Argument 1 to "iter_variants" of "PackageRepository" has incompatible type "Resource"; expected "PackageResource" [arg-type] -src/rez/packages.py:0: error: Incompatible types in assignment (expression has type "Set[str]", base class "ResourceWrapper" defined the type as "None") [assignment] -src/rez/packages.py:0: error: Argument 1 to "get_parent_package" of "PackageRepository" has incompatible type "Resource"; expected "VariantResource" [arg-type] -src/rez/packages.py:0: error: Argument 1 to "Package" has incompatible type "PackageRepositoryResource"; expected "PackageResource" [arg-type] -src/rez/packages.py:0: error: Argument 1 to "install_variant" of "PackageRepository" has incompatible type "Resource"; expected "VariantResource" [arg-type] -src/rez/packages.py:0: error: "Resource" has no attribute "_subpath" [attr-defined] -src/rez/packages.py:0: error: Argument 1 to "get_resource_from_handle" of "PackageRepositoryManager" has incompatible type "Union[ResourceHandle, Dict[Any, Any]]"; expected "ResourceHandle" [arg-type] -src/rez/packages.py:0: error: Argument 1 to "Package" has incompatible type "Resource"; expected "PackageResource" [arg-type] -src/rez/packages.py:0: error: Argument 1 to "get_resource_from_handle" of "PackageRepositoryManager" has incompatible type "Union[ResourceHandle, Dict[Any, Any]]"; expected "ResourceHandle" [arg-type] -src/rez/packages.py:0: error: Argument 1 to "Variant" has incompatible type "Resource"; expected "VariantResource" [arg-type] -src/rez/packages.py:0: error: "PackageFamily" has no attribute "name" [attr-defined] -src/rez/packages.py:0: error: "PackageFamily" has no attribute "name" [attr-defined] -src/rez/solver.py:0: error: "object" has no attribute "name" [attr-defined] -src/rez/solver.py:0: error: "object" has no attribute "version" [attr-defined] -src/rez/solver.py:0: error: "object" has no attribute "variant_index" [attr-defined] -src/rez/solver.py:0: error: "object" has no attribute "dependency" [attr-defined] -src/rez/solver.py:0: error: "object" has no attribute "conflicting_request" [attr-defined] -src/rez/solver.py:0: error: "object" has no attribute "dependency" [attr-defined] -src/rez/solver.py:0: error: "object" has no attribute "conflicting_request" [attr-defined] -src/rez/solver.py:0: error: Argument 1 of "__eq__" is incompatible with supertype "object"; supertype defines the argument type as "object" [override] -src/rez/solver.py:0: error: Argument 1 of "__eq__" is incompatible with supertype "object"; supertype defines the argument type as "object" [override] -src/rez/solver.py:0: error: Argument 1 of "__eq__" is incompatible with supertype "object"; supertype defines the argument type as "object" [override] -src/rez/solver.py:0: error: Argument 1 of "__eq__" is incompatible with supertype "object"; supertype defines the argument type as "object" [override] -src/rez/solver.py:0: error: Unsupported operand types for < ("int" and "None") [operator] -src/rez/solver.py:0: error: Unsupported operand types for > ("int" and "None") [operator] -src/rez/solver.py:0: error: Unsupported left operand type for < ("None") [operator] -src/rez/solver.py:0: error: Incompatible types in assignment (expression has type "Tuple[int, List[Tuple[int, SupportsLessThan]], int, List[Tuple[SupportsLessThan, str]], Optional[int]]", variable has type "Tuple[List[Tuple[int, SupportsLessThan]], int, List[Tuple[SupportsLessThan, str]], Optional[int]]") [assignment] -src/rez/solver.py:0: error: Incompatible return value type (got "Tuple[List[Tuple[int, SupportsLessThan]], int, List[Tuple[SupportsLessThan, str]], Optional[int]]", expected "Tuple[SupportsLessThan, ...]") [return-value] -src/rez/solver.py:0: error: Incompatible return value type (got "Optional[Set[str]]", expected "Set[str]") [return-value] -src/rez/solver.py:0: error: Item "None" of "Optional[Requirement]" has no attribute "range" [union-attr] -src/rez/solver.py:0: error: Item "None" of "Optional[Requirement]" has no attribute "range" [union-attr] -src/rez/solver.py:0: error: Item "None" of "Optional[Requirement]" has no attribute "range" [union-attr] -src/rez/solver.py:0: error: Expected iterable as variadic argument [misc] -src/rez/solver.py:0: error: Item "None" of "Optional[_PackageVariantSlice]" has no attribute "intersect" [union-attr] -src/rez/solver.py:0: error: Item "None" of "Optional[_PackageVariantSlice]" has no attribute "reduce_by" [union-attr] -src/rez/solver.py:0: error: Item "None" of "Optional[_PackageVariantSlice]" has no attribute "extract" [union-attr] -src/rez/solver.py:0: error: Argument 1 to "len" has incompatible type "Optional[_PackageVariantSlice]"; expected "Sized" [arg-type] -src/rez/solver.py:0: error: Item "None" of "Optional[_PackageVariantSlice]" has no attribute "split" [union-attr] -src/rez/solver.py:0: error: Argument 1 to "len" has incompatible type "Optional[_PackageVariantSlice]"; expected "Sized" [arg-type] -src/rez/solver.py:0: error: Item "None" of "Optional[_PackageVariantSlice]" has no attribute "extractable" [union-attr] -src/rez/solver.py:0: error: Incompatible types in assignment (expression has type "RequirementList", variable has type "List[Requirement]") [assignment] -src/rez/solver.py:0: error: "List[Requirement]" has no attribute "conflict" [attr-defined] -src/rez/solver.py:0: error: "List[Requirement]" has no attribute "conflict" [attr-defined] -src/rez/solver.py:0: error: "List[Requirement]" has no attribute "get" [attr-defined] -src/rez/solver.py:0: error: Incompatible types in assignment (expression has type "Optional[_PackageScope]", variable has type "_PackageScope") [assignment] -src/rez/solver.py:0: error: "List[Requirement]" has no attribute "requirements" [attr-defined] -src/rez/solver.py:0: error: Incompatible types in assignment (expression has type "List[Tuple[int, int]]", variable has type "Set[Tuple[int, int]]") [assignment] -src/rez/solver.py:0: error: "Set[Tuple[int, int]]" has no attribute "append" [attr-defined] -src/rez/solver.py:0: error: Item "None" of "Optional[PackageVariant]" has no attribute "version" [union-attr] -src/rez/solver.py:0: error: Incompatible types in assignment (expression has type "dict_values[Any, Any]", variable has type "List[Any]") [assignment] -src/rez/solver.py:0: error: Incompatible types in assignment (expression has type "dict_values[str, _PackageScope]", variable has type "List[_PackageScope]") [assignment] -src/rez/solver.py:0: error: Argument 1 to "_get_dependency_order" has incompatible type "Optional[digraph]"; expected "digraph" [arg-type] -src/rez/solver.py:0: error: Argument 1 to "_add_request_node" has incompatible type "Optional[Requirement]"; expected "Requirement" [arg-type] -src/rez/solver.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "List[_ResolvePhase]") [assignment] -src/rez/solver.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "List[_ResolvePhase]") [assignment] -src/rez/solver.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "Dict[Any, Any]") [assignment] -src/rez/solver.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "bool") [assignment] -src/rez/solver.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "float") [assignment] -src/rez/solver.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "float") [assignment] -src/rez/solver.py:0: error: Item "None" of "Optional[_ResolvePhase]" has no attribute "get_graph" [union-attr] +src/rez/serialise.py:0: error: Incompatible default for parameter "filepath" (default has type "None", parameter has type "str") [assignment] +src/rez/serialise.py:0: error: Incompatible default for parameter "filepath" (default has type "None", parameter has type "str") [assignment] +src/rez/serialise.py:0: error: "Callable[[str, FileFormat, Any], Any]" has no attribute "forget" [attr-defined] +src/rezplugins/package_repository/memory.py:0: error: Return type "Iterator[MemoryPackageFamilyResource | None]" of "iter_package_families" incompatible with return type "Iterator[MemoryPackageFamilyResource]" in supertype "rez.package_repository.PackageRepository" [override] +src/rezplugins/package_repository/filesystem.py:0: error: Module "typing" has no attribute "Self" [attr-defined] +src/rez/packages.py:0: error: "PackageBaseResourceWrapper[PackageResourceHelper[Any]]" has no attribute "parent" [attr-defined] +src/rez/packages.py:0: error: "PackageBaseResourceWrapper[VariantResourceHelper]" has no attribute "parent" [attr-defined] +src/rez/packages.py:0: error: Incompatible return value type (got "SourceCode[Any] | bool", expected "bool") [return-value] +src/rez/packages.py:0: error: Incompatible return value type (got "SourceCode[Any] | bool", expected "bool") [return-value] +src/rez/packages.py:0: error: Value of type "list[list[PackageRequest]] | None" is not indexable [index] +src/rez/packages.py:0: error: Unsupported left operand type for + ("SourceCode[Any]") [operator] +src/rez/packages.py:0: error: No overload variant of "__add__" of "list" matches argument type "SourceCode[Any]" [operator] +src/rez/packages.py:0: error: Unsupported operand types for + ("list[Requirement]" and "list[PackageRequest]") [operator] +src/rez/packages.py:0: error: No overload variant of "__add__" of "list" matches argument type "SourceCode[Any]" [operator] +src/rez/packages.py:0: error: Unsupported operand types for + ("list[Requirement]" and "list[PackageRequest]") [operator] +src/rez/packages.py:0: error: Argument 1 to "get_resource_from_handle" of "PackageRepositoryManager" has incompatible type "ResourceHandle | dict[Any, Any]"; expected "ResourceHandle" [arg-type] +src/rez/packages.py:0: error: Argument 1 to "get_resource_from_handle" of "PackageRepositoryManager" has incompatible type "ResourceHandle | dict[Any, Any]"; expected "ResourceHandle" [arg-type] +src/rez/packages.py:0: error: Name "Literal" is not defined [name-defined] +src/rez/packages.py:0: error: Overloaded function signature 2 will never be matched: signature 1's parameter type(s) are the same or broader [overload-cannot-match] +src/rez/packages.py:0: error: Name "Literal" is not defined [name-defined] src/rez/package_order.py:0: error: Module "typing" has no attribute "Self" [attr-defined] -src/rez/package_order.py:0: error: "object" has no attribute "main_comparable" [attr-defined] -src/rez/package_order.py:0: error: "object" has no attribute "fallback_comparable" [attr-defined] -src/rez/package_order.py:0: error: "object" has no attribute "main_comparable" [attr-defined] -src/rez/package_order.py:0: error: "object" has no attribute "fallback_comparable" [attr-defined] -src/rez/package_order.py:0: error: Argument 1 to "sort_key" of "PackageOrder" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rez/package_order.py:0: error: Incompatible types in assignment (expression has type "str", base class "PackageOrder" defined the type as "None") [assignment] -src/rez/package_order.py:0: error: Incompatible types in assignment (expression has type "str", base class "PackageOrder" defined the type as "None") [assignment] -src/rez/package_order.py:0: error: Incompatible types in assignment (expression has type "str", base class "PackageOrder" defined the type as "None") [assignment] -src/rez/package_order.py:0: error: Incompatible types in assignment (expression has type "str", base class "PackageOrder" defined the type as "None") [assignment] -src/rez/package_order.py:0: error: Incompatible types in assignment (expression has type "str", base class "PackageOrder" defined the type as "None") [assignment] -src/rez/package_order.py:0: error: Value of type "Optional[List[VersionToken]]" is not indexable [index] -src/rez/package_maker.py:0: error: Argument 1 to "iter_packages" of "MemoryPackageRepository" has incompatible type "Optional[MemoryPackageFamilyResource]"; expected "MemoryPackageFamilyResource" [arg-type] -src/rez/package_maker.py:0: error: Incompatible types in assignment (expression has type "Iterator[Variant]", variable has type "List[Variant]") [assignment] -src/rez/package_maker.py:0: error: Item "None" of "Optional[Variant]" has no attribute "base" [union-attr] -src/rez/package_maker.py:0: error: Argument 1 has incompatible type "Optional[Variant]"; expected "Variant" [arg-type] -src/rez/package_maker.py:0: error: Item "None" of "Optional[Variant]" has no attribute "root" [union-attr] -src/rez/package_maker.py:0: error: Argument 1 has incompatible type "Optional[Variant]"; expected "Variant" [arg-type] +src/rez/package_maker.py:0: error: Name "Iterable" is not defined [name-defined] src/rez/package_filter.py:0: error: Module "typing" has no attribute "Self" [attr-defined] -src/rez/package_filter.py:0: error: Argument 1 to "add_exclusion" of "PackageFilter" has incompatible type "List[Rule]"; expected "Rule" [arg-type] -src/rez/package_filter.py:0: error: Argument 1 to "add_inclusion" of "PackageFilter" has incompatible type "List[Rule]"; expected "Rule" [arg-type] -src/rez/package_filter.py:0: error: Incompatible return value type (got "Tuple[Optional[str], List[Rule]]", expected "Tuple[str, List[Rule]]") [return-value] -src/rez/package_filter.py:0: error: "Rule" has no attribute "_family"; maybe "family"? [attr-defined] -src/rez/package_filter.py:0: error: Too many arguments for "RegexRuleBase" [call-arg] -src/rez/package_filter.py:0: error: Incompatible types in assignment (expression has type "str", base class "Rule" defined the type as "None") [assignment] -src/rez/package_filter.py:0: error: Incompatible types in assignment (expression has type "str", base class "Rule" defined the type as "None") [assignment] -src/rez/package_filter.py:0: error: Incompatible types in assignment (expression has type "str", base class "Rule" defined the type as "None") [assignment] -src/rez/package_filter.py:0: error: Incompatible types in assignment (expression has type "str", base class "Rule" defined the type as "None") [assignment] -src/rez/package_cache.py:0: error: Value of type "Optional[Dict[str, Any]]" is not indexable [index] src/rez/package_cache.py:0: error: Module has no attribute "CREATE_NEW_PROCESS_GROUP" [attr-defined] -src/rez/developer_package.py:0: error: Unsupported left operand type for | ("None") [operator] -src/rez/developer_package.py:0: error: Argument 1 to "from_path" of "DeveloperPackage" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rez/resolver.py:0: error: Name "digraph" is not defined [name-defined] -src/rez/resolver.py:0: error: Name "digraph" is not defined [name-defined] -src/rez/resolver.py:0: error: Name "ResourceHandle" is not defined [name-defined] -src/rez/resolver.py:0: error: Incompatible return value type (got "Tuple[str, Union[Any, _Miss]]", expected "Tuple[str, Tuple[SolverDict, Dict[Any, Any], Dict[Any, Any]]]") [return-value] -src/rez/resolver.py:0: error: Argument 1 to "get_variant_state_handle" of "PackageRepository" has incompatible type "Resource"; expected "PackageResource" [arg-type] -src/rez/resolver.py:0: error: Item "None" of "Optional[List[Variant]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/resolver.py:0: error: Argument 1 to "get_variant_state_handle" of "PackageRepository" has incompatible type "Union[Resource, Any]"; expected "PackageResource" [arg-type] -src/rez/resolver.py:0: error: Incompatible types in assignment (expression has type "Optional[ResolverStatus]", variable has type "ResolverStatus") [assignment] -src/rez/resolver.py:0: error: Item "None" of "Optional[List[PackageVariant]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/resolver.py:0: error: Item "None" of "Optional[List[Requirement]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/resolved_context.py:0: error: Argument "timestamp" to "Resolver" has incompatible type "Optional[float]"; expected "Optional[int]" [arg-type] -src/rez/resolved_context.py:0: error: Incompatible types in assignment (expression has type "Optional[float]", variable has type "float") [assignment] -src/rez/resolved_context.py:0: error: Incompatible types in assignment (expression has type "Optional[float]", variable has type "float") [assignment] -src/rez/resolved_context.py:0: error: Item "None" of "Optional[List[Variant]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/resolved_context.py:0: error: Item "None" of "Optional[List[Variant]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/resolved_context.py:0: error: Argument 1 to "get_equivalent_variant" of "PackageRepository" has incompatible type "Resource"; expected "VariantResource" [arg-type] -src/rez/resolved_context.py:0: error: Item "None" of "Optional[List[Variant]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/resolved_context.py:0: error: Item "None" of "Optional[List[PackageRequest]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/resolved_context.py:0: error: Item "None" of "Optional[List[Variant]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/resolved_context.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "PackageRequest") [assignment] -src/rez/resolved_context.py:0: error: Incompatible return value type (got "List[Union[Requirement, PackageRequest]]", expected "List[Union[Requirement, PackageRequest, str]]") [return-value] -src/rez/resolved_context.py:0: error: Argument 1 to "read_graph_from_string" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rez/resolved_context.py:0: error: Argument 1 to "write_dot" has incompatible type "Optional[digraph]"; expected "digraph" [arg-type] -src/rez/resolved_context.py:0: error: Item "None" of "Optional[List[Variant]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/resolved_context.py:0: error: Item "None" of "Optional[List[Variant]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/resolved_context.py:0: error: Item "None" of "Optional[List[Variant]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/resolved_context.py:0: error: Item "None" of "Optional[List[Requirement]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/resolved_context.py:0: error: Item "None" of "Optional[List[Variant]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/resolved_context.py:0: error: Item "None" of "Optional[List[Variant]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/resolved_context.py:0: error: Item "None" of "Optional[List[Variant]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/resolved_context.py:0: error: Argument 1 to "join" of "Shell" has incompatible type "Union[str, Sequence[str], None]"; expected "Iterable[str]" [arg-type] -src/rez/resolved_context.py:0: error: Item "None" of "Optional[List[Variant]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/resolved_context.py:0: error: Incompatible types in string interpolation (expression has type "str", placeholder has type "Union[int, float, SupportsInt]") [str-format] -src/rez/resolved_context.py:0: error: Argument 1 to "update" of "MutableMapping" has incompatible type "Optional[Dict[str, Any]]"; expected "SupportsKeysAndGetItem[str, Any]" [arg-type] -src/rez/resolved_context.py:0: error: Item "_NeverError" of "Union[SourceCodeError, _NeverError]" has no attribute "short_msg" [union-attr] -src/rez/wrapper.py:0: error: Incompatible types in assignment (expression has type "Optional[Callable[[Any], Any]]", variable has type "Callable[[Any], Any]") [assignment] -src/rez/suite.py:0: error: Item "None" of "Optional[defaultdict[str, List[Tool]]]" has no attribute "get" [union-attr] -src/rez/suite.py:0: error: Item "None" of "Optional[List[Tool]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/suite.py:0: error: Item "None" of "Optional[defaultdict[str, List[Tool]]]" has no attribute "values" [union-attr] -src/rez/suite.py:0: error: Argument 1 to "append" of "list" has incompatible type "Tuple[str, str, str, str, str]"; expected "List[str]" [arg-type] -src/rez/suite.py:0: error: Argument 1 to "append" of "list" has incompatible type "Callable[[Any], Any]"; expected "None" [arg-type] -src/rez/suite.py:0: error: Argument 1 to "append" of "list" has incompatible type "Callable[[Any], Any]"; expected "None" [arg-type] -src/rezplugins/package_repository/memory.py:0: error: "PackageRepository" has no attribute "data" [attr-defined] -src/rezplugins/package_repository/memory.py:0: error: Incompatible types in "yield" (actual type "Resource", expected type "MemoryPackageResource") [misc] -src/rezplugins/package_repository/memory.py:0: error: Incompatible types in "yield" (actual type "Resource", expected type "MemoryPackageResource") [misc] -src/rezplugins/package_repository/memory.py:0: error: Incompatible types in assignment (expression has type "str", base class "PackageResourceHelper" defined the type as "None") [assignment] -src/rezplugins/package_repository/memory.py:0: error: Argument 1 to "construct" of "VersionedObject" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rezplugins/package_repository/memory.py:0: error: Incompatible return value type (got "Resource", expected "PackageRepositoryResource") [return-value] -src/rezplugins/package_repository/memory.py:0: error: "PackageRepository" has no attribute "data" [attr-defined] -src/rezplugins/package_repository/memory.py:0: error: Incompatible return value type (got "Resource", expected "PackageRepositoryResource") [return-value] -src/rezplugins/package_repository/memory.py:0: error: Incompatible return value type (got "Resource", expected "Optional[MemoryPackageFamilyResource]") [return-value] -src/rezplugins/package_repository/memory.py:0: error: Return type "Iterator[Optional[MemoryPackageFamilyResource]]" of "iter_package_families" incompatible with return type "Iterator[PackageFamilyResource]" in supertype "PackageRepository" [override] -src/rezplugins/package_repository/memory.py:0: error: Argument 1 of "iter_packages" is incompatible with supertype "PackageRepository"; supertype defines the argument type as "PackageFamilyResource" [override] -src/rezplugins/package_repository/memory.py:0: error: Argument 1 of "iter_variants" is incompatible with supertype "PackageRepository"; supertype defines the argument type as "PackageResource" [override] -src/rezplugins/package_repository/memory.py:0: error: Incompatible return value type (got "PackageRepositoryResource", expected "PackageFamilyResource") [return-value] -src/rez/build_process.py:0: error: Item "None" of "Optional[ReleaseVCS]" has no attribute "get_changelog" [union-attr] -src/rez/build_system.py:0: error: Unsupported operand types for - ("Set[Type[BuildSystem]]" and "Set[Optional[str]]") [operator] -src/rezplugins/shell/sh.py:0: error: Incompatible return value type (got "Tuple[bool, bool, bool, bool]", expected "Tuple[Union[str, Literal[False], None], bool, bool, bool]") [return-value] -src/rezplugins/shell/csh.py:0: error: Return type "Tuple[Union[bool, str], bool, bool, bool]" of "startup_capabilities" incompatible with return type "Tuple[Union[str, Literal[False], None], bool, bool, bool]" in supertype "Shell" [override] -src/rezplugins/shell/csh.py:0: error: Argument 1 of "startup_capabilities" is incompatible with supertype "Shell"; supertype defines the argument type as "Union[str, Literal[False], None]" [override] -src/rezplugins/shell/cmd.py:0: error: Argument 1 of "startup_capabilities" is incompatible with supertype "Shell"; supertype defines the argument type as "Union[str, Literal[False], None]" [override] -src/rezplugins/shell/cmd.py:0: error: Signature of "spawn_shell" incompatible with supertype "Shell" [override] -src/rezplugins/shell/cmd.py:0: error: Argument 1 of "escape_string" is incompatible with supertype "ActionInterpreter"; supertype defines the argument type as "Union[str, EscapedString]" [override] +src/rez/developer_package.py:0: error: Argument 1 to "from_path" of "DeveloperPackage" has incompatible type "str | None"; expected "str" [arg-type] +src/rez/resolved_context.py:0: error: Argument 1 to "len" has incompatible type "Version | None"; expected "Sized" [arg-type] +src/rez/resolved_context.py:0: error: Item "None" of "Version | None" has no attribute "trim" [union-attr] +src/rez/resolved_context.py:0: error: Value of type "Version | None" is not indexable [index] +src/rez/resolved_context.py:0: error: Argument 1 to "len" has incompatible type "Version | None"; expected "Sized" [arg-type] +src/rez/resolved_context.py:0: error: Value of type "Version | None" is not indexable [index] +src/rez/resolved_context.py:0: error: Argument 1 to "len" has incompatible type "Version | None"; expected "Sized" [arg-type] +src/rez/resolved_context.py:0: error: Value of type "Version | None" is not indexable [index] +src/rez/resolved_context.py:0: error: Argument 1 to "len" has incompatible type "Version | None"; expected "Sized" [arg-type] +src/rez/resolved_context.py:0: error: Argument 1 to "normalized" has incompatible type "str | None"; expected "str" [arg-type] +src/rez/resolved_context.py:0: error: Argument 1 to "VersionBinding" has incompatible type "Version | None"; expected "Version" [arg-type] +src/rez/resolved_context.py:0: error: Argument 1 to "normalized" has incompatible type "str | None"; expected "str" [arg-type] +src/rez/wrapper.py:0: error: Incompatible types in assignment (expression has type "Callable[[Any], Any] | None", variable has type "Callable[[Any], Any]") [assignment] +src/rez/suite.py:0: error: Name "Callable" is not defined; did you mean "callable"? [name-defined] +src/rez/suite.py:0: error: Item "None" of "list[Tool] | None" has no attribute "__iter__" (not iterable) [union-attr] +src/rez/suite.py:0: error: Item "None" of "defaultdict[str, list[Tool]] | None" has no attribute "values" [union-attr] +src/rez/build_system.py:0: error: Incompatible types in assignment (expression has type "str | None", variable has type "str") [assignment] +src/rezplugins/shell/csh.py:0: error: Return type "tuple[bool | str, bool, bool, bool]" of "startup_capabilities" incompatible with return type "tuple[str | Literal[False] | None, bool, bool, bool]" in supertype "rez.shells.Shell" [override] +src/rezplugins/shell/csh.py:0: error: Argument 1 of "startup_capabilities" is incompatible with supertype "rez.shells.Shell"; supertype defines the argument type as "str | Literal[False] | None" [override] +src/rezplugins/shell/cmd.py:0: error: Argument 1 of "startup_capabilities" is incompatible with supertype "rez.shells.Shell"; supertype defines the argument type as "str | Literal[False] | None" [override] +src/rezplugins/shell/cmd.py:0: error: Signature of "spawn_shell" incompatible with supertype "rez.shells.Shell" [override] +src/rezplugins/shell/cmd.py:0: error: Argument 1 of "escape_string" is incompatible with supertype "rez.rex.ActionInterpreter"; supertype defines the argument type as "str | EscapedString" [override] +src/rezplugins/shell/cmd.py:0: error: Incompatible types in assignment (expression has type "EscapedString", variable has type "str") [assignment] +src/rezplugins/shell/cmd.py:0: error: "str" has no attribute "expanduser" [attr-defined] +src/rezplugins/shell/cmd.py:0: error: "str" has no attribute "strings" [attr-defined] src/rezplugins/shell/cmd.py:0: error: "setenv" of "CMD" does not return a value (it only ever returns None) [func-returns-value] -src/rezplugins/shell/_utils/powershell_base.py:0: error: Argument 1 of "startup_capabilities" is incompatible with supertype "Shell"; supertype defines the argument type as "Union[str, Literal[False], None]" [override] -src/rezplugins/shell/_utils/powershell_base.py:0: error: Signature of "spawn_shell" incompatible with supertype "Shell" [override] +src/rezplugins/shell/cmd.py:0: error: Argument 1 to "_addline" of "Shell" has incompatible type "None"; expected "str" [arg-type] +src/rezplugins/shell/_utils/powershell_base.py:0: error: Argument 1 of "startup_capabilities" is incompatible with supertype "rez.shells.Shell"; supertype defines the argument type as "str | Literal[False] | None" [override] +src/rezplugins/shell/_utils/powershell_base.py:0: error: Signature of "spawn_shell" incompatible with supertype "rez.shells.Shell" [override] src/rezplugins/shell/_utils/powershell_base.py:0: error: "setenv" of "PowerShellBase" does not return a value (it only ever returns None) [func-returns-value] +src/rezplugins/shell/_utils/powershell_base.py:0: error: Argument 1 to "_addline" of "Shell" has incompatible type "None"; expected "str" [arg-type] src/rezplugins/release_vcs/hg.py:0: error: Incompatible types in assignment (expression has type "None", variable has type "str") [assignment] -src/rezplugins/release_vcs/git.py:0: error: Item "None" of "Optional[Any]" has no attribute "allow_no_upstream" [union-attr] +src/rezplugins/release_vcs/git.py:0: error: Item "None" of "Any | None" has no attribute "allow_no_upstream" [union-attr] src/rezplugins/release_vcs/git.py:0: error: Name "retain_cwd" is not defined [name-defined] src/rezplugins/release_vcs/git.py:0: error: Name "git" is not defined [name-defined] src/rezplugins/release_vcs/git.py:0: error: Name "git" is not defined [name-defined] -src/rezplugins/release_hook/emailer.py:0: error: Signature of "post_release" incompatible with supertype "ReleaseHook" [override] -src/rezplugins/release_hook/emailer.py:0: error: Item "None" of "Optional[Any]" has no attribute "body" [union-attr] -src/rezplugins/release_hook/emailer.py:0: error: Item "None" of "Optional[Any]" has no attribute "subject" [union-attr] -src/rezplugins/release_hook/emailer.py:0: error: Item "None" of "Optional[Any]" has no attribute "recipients" [union-attr] -src/rezplugins/release_hook/emailer.py:0: error: Item "None" of "Optional[Any]" has no attribute "smtp_host" [union-attr] -src/rezplugins/release_hook/emailer.py:0: error: Item "None" of "Optional[Any]" has no attribute "sender" [union-attr] -src/rezplugins/release_hook/emailer.py:0: error: Item "None" of "Optional[Any]" has no attribute "smtp_host" [union-attr] -src/rezplugins/release_hook/emailer.py:0: error: Item "None" of "Optional[Any]" has no attribute "smtp_port" [union-attr] -src/rezplugins/release_hook/emailer.py:0: error: Item "None" of "Optional[Any]" has no attribute "sender" [union-attr] -src/rezplugins/release_hook/emailer.py:0: error: Item "None" of "Optional[Any]" has no attribute "recipients" [union-attr] -src/rezplugins/release_hook/command.py:0: error: Item "None" of "Optional[Any]" has no attribute "print_error" [union-attr] -src/rezplugins/release_hook/command.py:0: error: Item "None" of "Optional[Any]" has no attribute "print_output" [union-attr] -src/rezplugins/release_hook/command.py:0: error: Signature of "pre_build" incompatible with supertype "ReleaseHook" [override] -src/rezplugins/release_hook/command.py:0: error: Item "None" of "Optional[Any]" has no attribute "pre_build_commands" [union-attr] -src/rezplugins/release_hook/command.py:0: error: Item "None" of "Optional[Any]" has no attribute "cancel_on_error" [union-attr] -src/rezplugins/release_hook/command.py:0: error: Signature of "pre_release" incompatible with supertype "ReleaseHook" [override] -src/rezplugins/release_hook/command.py:0: error: Item "None" of "Optional[Any]" has no attribute "pre_release_commands" [union-attr] -src/rezplugins/release_hook/command.py:0: error: Item "None" of "Optional[Any]" has no attribute "cancel_on_error" [union-attr] -src/rezplugins/release_hook/command.py:0: error: Signature of "post_release" incompatible with supertype "ReleaseHook" [override] -src/rezplugins/release_hook/command.py:0: error: Item "None" of "Optional[Any]" has no attribute "post_release_commands" [union-attr] -src/rezplugins/release_hook/command.py:0: error: Argument 1 to "append" of "list" has incompatible type "Dict[Any, Any]"; expected "int" [arg-type] -src/rezplugins/release_hook/command.py:0: error: Item "None" of "Optional[Any]" has no attribute "print_commands" [union-attr] -src/rezplugins/release_hook/command.py:0: error: Item "None" of "Optional[Any]" has no attribute "print_commands" [union-attr] -src/rezplugins/release_hook/command.py:0: error: Item "None" of "Optional[Any]" has no attribute "stop_on_error" [union-attr] -src/rezplugins/release_hook/amqp.py:0: error: Signature of "post_release" incompatible with supertype "ReleaseHook" [override] -src/rezplugins/release_hook/amqp.py:0: error: Item "None" of "Optional[Any]" has no attribute "message_attributes" [union-attr] -src/rezplugins/release_hook/amqp.py:0: error: Item "None" of "Optional[Any]" has no attribute "host" [union-attr] -src/rezplugins/release_hook/amqp.py:0: error: Item "None" of "Optional[Any]" has no attribute "exchange_routing_key" [union-attr] -src/rezplugins/release_hook/amqp.py:0: error: Item "None" of "Optional[Any]" has no attribute "host" [union-attr] -src/rezplugins/package_repository/filesystem.py:0: error: Module "typing" has no attribute "Self" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 1 to "join" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 2 to "join" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rezplugins/package_repository/filesystem.py:0: error: "PackageRepository" has no attribute "_get_file" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible types in "yield" (actual type "Resource", expected type "FileSystemPackageResource") [misc] -src/rezplugins/package_repository/filesystem.py:0: error: "PackageRepository" has no attribute "_get_version_dirs" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: Item "None" of "Optional[Any]" has no attribute "check_package_definition_files" [union-attr] -src/rezplugins/package_repository/filesystem.py:0: error: "PackageRepository" has no attribute "_get_file" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible types in "yield" (actual type "Resource", expected type "FileSystemPackageResource") [misc] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible types in assignment (expression has type "str", base class "PackageResourceHelper" defined the type as "None") [assignment] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible return value type (got "Optional[str]", expected "str") [return-value] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible return value type (got "Resource", expected "FileSystemPackageFamilyResource") [return-value] -src/rezplugins/package_repository/filesystem.py:0: error: Item "None" of "Optional[Dict[str, Any]]" has no attribute "get" [union-attr] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 1 to "join" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 2 to "join" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rezplugins/package_repository/filesystem.py:0: error: "PackageRepository" has no attribute "_get_file" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: "PackageRepository" has no attribute "disable_memcache" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible return value type (got "Resource", expected "FileSystemPackageResource") [return-value] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 1 to "join" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rezplugins/package_repository/filesystem.py:0: error: "FileSystemCombinedPackageFamilyResource" has no attribute "versions" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible types in "yield" (actual type "Resource", expected type "FileSystemCombinedPackageResource") [misc] -src/rezplugins/package_repository/filesystem.py:0: error: "FileSystemCombinedPackageFamilyResource" has no attribute "versions" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible types in "yield" (actual type "Resource", expected type "FileSystemCombinedPackageResource") [misc] -src/rezplugins/package_repository/filesystem.py:0: error: "PackageRepository" has no attribute "disable_memcache" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible types in assignment (expression has type "str", base class "PackageResourceHelper" defined the type as "None") [assignment] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible return value type (got "Resource", expected "FileSystemCombinedPackageFamilyResource") [return-value] -src/rezplugins/package_repository/filesystem.py:0: error: Item "None" of "Optional[Dict[str, Any]]" has no attribute "get" [union-attr] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible types in "yield" (actual type "Resource", expected type "FileSystemCombinedVariantResource") [misc] -src/rezplugins/package_repository/filesystem.py:0: error: Return type "Optional[Dict[str, Any]]" of "_load" incompatible with return type "Dict[str, Any]" in supertype "Resource" [override] -src/rezplugins/package_repository/filesystem.py:0: error: Item "None" of "Optional[Dict[str, Any]]" has no attribute "copy" [union-attr] -src/rezplugins/package_repository/filesystem.py:0: error: "FileSystemCombinedPackageFamilyResource" has no attribute "version_overrides" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible return value type (got "Resource", expected "PackageRepositoryResource") [return-value] -src/rezplugins/package_repository/filesystem.py:0: error: Cannot assign to a method [method-assign] -src/rezplugins/package_repository/filesystem.py:0: error: Cannot assign to a method [method-assign] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 1 to "append" of "list" has incompatible type "int"; expected "str" [arg-type] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible return value type (got "Optional[PackageFamilyResource]", expected "PackageFamilyResource") [return-value] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 1 of "iter_variants" is incompatible with supertype "PackageRepository"; supertype defines the argument type as "PackageResource" [override] -src/rezplugins/package_repository/filesystem.py:0: error: Return type "PackageRepositoryResource" of "get_parent_package_family" incompatible with return type "PackageFamilyResource" in supertype "PackageRepository" [override] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 1 of "get_variant_state_handle" is incompatible with supertype "PackageRepository"; supertype defines the argument type as "PackageResource" [override] -src/rezplugins/package_repository/filesystem.py:0: error: "PackageRepositoryResource" has no attribute "state_handle" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: "PackageFamilyResource" has no attribute "get_last_release_time" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible types in assignment (expression has type "str", variable has type "Optional[int]") [assignment] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 2 to "join" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 1 to "remove_package" of "FileSystemPackageRepository" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rezplugins/package_repository/filesystem.py:0: error: Item "None" of "Optional[Any]" has no attribute "file_lock_dir" [union-attr] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 1 of "pre_variant_install" is incompatible with supertype "PackageRepository"; supertype defines the argument type as "VariantResource" [override] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 2 to "join" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible return value type (got "Optional[VariantResource]", expected "VariantResource") [return-value] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 1 to "_lock_package" of "FileSystemPackageRepository" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rezplugins/package_repository/filesystem.py:0: error: Item "None" of "Optional[Any]" has no attribute "file_lock_type" [union-attr] -src/rezplugins/package_repository/filesystem.py:0: error: Item "None" of "Optional[Any]" has no attribute "file_lock_type" [union-attr] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible import of "LockFile" (imported name has type "Type[MkdirLockFile]", local name has type "Type[LinkLockFile]") [assignment] -src/rezplugins/package_repository/filesystem.py:0: error: Item "None" of "Optional[Any]" has no attribute "file_lock_type" [union-attr] -src/rezplugins/package_repository/filesystem.py:0: error: Item "None" of "Optional[Any]" has no attribute "file_lock_type" [union-attr] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible import of "LockFile" (imported name has type "Type[SymlinkLockFile]", local name has type "Type[LinkLockFile]") [assignment] -src/rezplugins/package_repository/filesystem.py:0: error: Item "None" of "Optional[Any]" has no attribute "file_lock_timeout" [union-attr] -src/rezplugins/package_repository/filesystem.py:0: error: "Callable[[], List[Tuple[str, Optional[str]]]]" has no attribute "forget" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: "Callable[[str], List[str]]" has no attribute "forget" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: Item "None" of "Optional[Any]" has no attribute "check_package_definition_files" [union-attr] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible return value type (got "List[Resource]", expected "List[PackageFamilyResource]") [return-value] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible return value type (got "Resource", expected "Optional[PackageFamilyResource]") [return-value] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible return value type (got "Resource", expected "Optional[PackageFamilyResource]") [return-value] -src/rezplugins/package_repository/filesystem.py:0: error: Item "None" of "Optional[FileFormat]" has no attribute "extension" [union-attr] -src/rezplugins/package_repository/filesystem.py:0: error: "PackageFamilyResource" has no attribute "iter_packages" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: Item "None" of "Optional[Any]" has no attribute "package_filenames" [union-attr] -src/rezplugins/package_repository/filesystem.py:0: error: Incompatible default for argument "overrides" (default has type "None", argument has type "Dict[str, Any]") [assignment] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 1 to "get_package_family" of "FileSystemPackageRepository" has incompatible type "Union[Any, str, None]"; expected "str" [arg-type] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 1 to "_create_family" of "FileSystemPackageRepository" has incompatible type "Union[Any, str, None]"; expected "str" [arg-type] -src/rezplugins/package_repository/filesystem.py:0: error: "VariantResource" has no attribute "uuid" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 1 to "_get_package_data" has incompatible type "PackageRepositoryResource"; expected "PackageRepositoryResourceWrapper" [arg-type] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 1 to "iter_variants" of "FileSystemPackageRepository" has incompatible type "Package"; expected "PackageResourceHelper" [arg-type] -src/rezplugins/package_repository/filesystem.py:0: error: "VariantResource" has no attribute "variant_requires" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 1 to "iter_variants" of "FileSystemPackageRepository" has incompatible type "Package"; expected "PackageResourceHelper" [arg-type] -src/rezplugins/package_repository/filesystem.py:0: error: Item "None" of "Optional[Any]" has no attribute "get" [union-attr] -src/rezplugins/package_repository/filesystem.py:0: error: Item "None" of "Optional[Any]" has no attribute "package_filenames" [union-attr] -src/rezplugins/package_repository/filesystem.py:0: error: "VariantResource" has no attribute "variant_requires" [attr-defined] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 2 to "join" has incompatible type "Union[Any, str, None]"; expected "str" [arg-type] -src/rezplugins/package_repository/filesystem.py:0: error: Argument 1 to "_on_changed" of "FileSystemPackageRepository" has incompatible type "Union[Any, str, None]"; expected "str" [arg-type] +src/rezplugins/release_hook/emailer.py:0: error: Signature of "post_release" incompatible with supertype "rez.release_hook.ReleaseHook" [override] +src/rezplugins/release_hook/command.py:0: error: Signature of "pre_build" incompatible with supertype "rez.release_hook.ReleaseHook" [override] +src/rezplugins/release_hook/command.py:0: error: Signature of "pre_release" incompatible with supertype "rez.release_hook.ReleaseHook" [override] +src/rezplugins/release_hook/command.py:0: error: Signature of "post_release" incompatible with supertype "rez.release_hook.ReleaseHook" [override] +src/rezplugins/release_hook/command.py:0: error: Argument 1 to "append" of "list" has incompatible type "dict[Any, Any]"; expected "int" [arg-type] +src/rezplugins/release_hook/amqp.py:0: error: Signature of "post_release" incompatible with supertype "rez.release_hook.ReleaseHook" [override] src/rezplugins/build_system/custom.py:0: error: "Popen[Any]" object is not iterable [misc] src/rezplugins/build_system/custom.py:0: error: Cannot determine type of "retcode" [has-type] src/rezplugins/build_system/custom.py:0: error: "Popen[Any]" object is not iterable [misc] @@ -503,90 +155,78 @@ src/rezplugins/build_system/custom.py:0: error: Cannot determine type of "retcod src/rezplugins/build_system/cmake.py:0: error: "Popen[Any]" object is not iterable [misc] src/rezplugins/build_system/cmake.py:0: error: Cannot determine type of "retcode" [has-type] src/rezplugins/build_system/cmake.py:0: error: "Popen[Any]" object is not iterable [misc] -src/rezplugins/build_system/cmake.py:0: error: Cannot determine type of "retcode'" [has-type] +src/rezplugins/build_system/cmake.py:0: error: Cannot determine type of "retcode" [has-type] src/rezplugins/build_system/cmake.py:0: error: "Popen[Any]" object is not iterable [misc] -src/rezplugins/build_system/cmake.py:0: error: Cannot determine type of "retcode'" [has-type] +src/rezplugins/build_system/cmake.py:0: error: Cannot determine type of "retcode" [has-type] src/rezplugins/build_system/cmake.py:0: error: "Popen[Any]" object is not iterable [misc] src/rezplugins/build_system/cmake.py:0: error: Cannot determine type of "retcode" [has-type] -src/rez/status.py:0: error: Argument 1 to "append" of "list" has incompatible type "List[Any]"; expected "Tuple[str, str, str, str, str, Optional[Callable[[str], str]]]" [arg-type] -src/rez/status.py:0: error: Argument 2 to "print_colored_columns" has incompatible type "List[Union[Tuple[str, str, str, str, str, Optional[Callable[[str], str]]], List[Optional[str]]]]"; expected "Sequence[Tuple[Any, ...]]" [arg-type] -src/rez/status.py:0: error: Argument 1 to "append" of "list" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rez/package_test.py:0: error: Incompatible types in assignment (expression has type "Optional[Package]", variable has type "Package") [assignment] -src/rez/package_test.py:0: error: Item "None" of "Optional[ResolvedContext]" has no attribute "get_resolved_package" [union-attr] -src/rez/package_test.py:0: error: Item "None" of "Union[Variant, Any, None]" has no attribute "uri" [union-attr] -src/rez/package_test.py:0: error: Item "None" of "Union[Variant, Any, None]" has no attribute "variant_requires" [union-attr] -src/rez/package_test.py:0: error: Item "None" of "Union[Variant, Any, None]" has no attribute "handle" [union-attr] -src/rez/package_test.py:0: error: Item "None" of "Union[Variant, Any, None]" has no attribute "uri" [union-attr] -src/rez/package_test.py:0: error: Item "None" of "Union[Variant, Any, None]" has no attribute "format" [union-attr] -src/rez/package_test.py:0: error: Item "None" of "Union[Variant, Any, None]" has no attribute "format" [union-attr] -src/rez/package_search.py:0: error: "PackageFamily" has no attribute "name" [attr-defined] -src/rez/package_search.py:0: error: Incompatible types in assignment (expression has type "Iterator[Package]", variable has type "Iterator[PackageFamily]") [assignment] -src/rez/package_search.py:0: error: "PackageFamily" has no attribute "version" [attr-defined] -src/rez/package_search.py:0: error: "PackageFamily" has no attribute "iter_variants" [attr-defined] -src/rez/package_search.py:0: error: "PackageFamily" has no attribute "name" [attr-defined] -src/rez/package_search.py:0: error: "PackageFamily" has no attribute "name" [attr-defined] -src/rez/package_search.py:0: error: "PackageFamily" has no attribute "name" [attr-defined] -src/rez/package_search.py:0: error: "PackageFamily" has no attribute "name" [attr-defined] -src/rez/package_search.py:0: error: Argument 2 to "expand_abbreviations" has incompatible type "Tuple[str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str]"; expected "List[str]" [arg-type] -src/rez/package_search.py:0: error: Incompatible types in assignment (expression has type "Tuple[Any, Callable[[Any], Any]]", variable has type "str") [assignment] -src/rez/package_search.py:0: error: Argument 1 to "append" of "list" has incompatible type "str"; expected "Tuple[Any, Callable[[Any], Any]]" [arg-type] -src/rez/package_help.py:0: error: Item "None" of "Optional[Variant]" has no attribute "base" [union-attr] -src/rez/package_help.py:0: error: Item "None" of "Optional[Variant]" has no attribute "root" [union-attr] -src/rez/package_copy.py:0: error: Argument 1 to "install_variant" of "PackageRepository" has incompatible type "Resource"; expected "VariantResource" [arg-type] -src/rez/package_copy.py:0: error: Argument 1 to "pre_variant_install" of "PackageRepository" has incompatible type "Resource"; expected "VariantResource" [arg-type] -src/rez/package_copy.py:0: error: Argument "variant_resource" to "install_variant" of "PackageRepository" has incompatible type "Resource"; expected "VariantResource" [arg-type] -src/rez/package_copy.py:0: error: Item "None" of "Optional[Variant]" has no attribute "uri" [union-attr] -src/rez/package_bind.py:0: error: List comprehension has incompatible type List[Tuple[Any, Any]]; expected List[List[str]] [misc] -src/rez/utils/pip.py:0: error: Incompatible types in assignment (expression has type "Optional[VersionRange]", variable has type "VersionRange") [assignment] +src/rez/status.py:0: error: Unsupported operand types for in ("Version | None" and "VersionRange") [operator] +src/rez/status.py:0: error: Argument 1 to "append" of "list" has incompatible type "str | None"; expected "str" [arg-type] +src/rez/package_test.py:0: error: Item "None" of "ResolvedContext | None" has no attribute "get_resolved_package" [union-attr] +src/rez/package_test.py:0: error: Item "None" of "Variant | Any | None" has no attribute "uri" [union-attr] +src/rez/package_test.py:0: error: Item "None" of "Variant | Any | None" has no attribute "variant_requires" [union-attr] +src/rez/package_test.py:0: error: Item "None" of "Variant | Any | None" has no attribute "handle" [union-attr] +src/rez/package_test.py:0: error: Item "None" of "Variant | Any | None" has no attribute "uri" [union-attr] +src/rez/package_test.py:0: error: Item "None" of "Variant | Any | None" has no attribute "format" [union-attr] +src/rez/package_test.py:0: error: Item "None" of "Variant | Any | None" has no attribute "format" [union-attr] +src/rez/package_search.py:0: error: Item "SourceCode[Any]" of "SourceCode[Any] | list[str]" has no attribute "__iter__" (not iterable) [union-attr] +src/rez/package_search.py:0: error: Incompatible types in assignment (expression has type "list[Any]", variable has type "set[Any]") [assignment] +src/rez/package_search.py:0: error: Argument 2 to "expand_abbreviations" has incompatible type "tuple[str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str, str]"; expected "list[str]" [arg-type] +src/rez/package_search.py:0: error: Incompatible types in assignment (expression has type "tuple[Any, Callable[[Any], Any]]", variable has type "str") [assignment] +src/rez/package_search.py:0: error: Argument 1 to "append" of "list" has incompatible type "str"; expected "tuple[Any, Callable[[Any], Any]]" [arg-type] +src/rez/package_help.py:0: error: Item "None" of "Variant | None" has no attribute "base" [union-attr] +src/rez/package_help.py:0: error: Item "None" of "Variant | None" has no attribute "root" [union-attr] +src/rez/package_copy.py:0: error: Incompatible types in assignment (expression has type "set[int]", variable has type "list[int] | None") [assignment] +src/rez/package_copy.py:0: error: Item "None" of "Variant | None" has no attribute "uri" [union-attr] +src/rez/package_copy.py:0: error: Argument 2 to "join" has incompatible type "str | None"; expected "str" [arg-type] +src/rez/package_copy.py:0: error: Argument 1 to "join" has incompatible type "str | None"; expected "str" [arg-type] +src/rez/package_bind.py:0: error: Incompatible types in assignment (expression has type "list[Any]", variable has type "set[Any]") [assignment] +src/rez/utils/pip.py:0: error: Incompatible types in assignment (expression has type "VersionRange | None", variable has type "VersionRange") [assignment] src/rez/utils/installer.py:0: error: Name "env" is not defined [name-defined] -src/rez/cli/selftest.py:0: error: Argument 1 to "getfile" has incompatible type "Optional[FrameType]"; expected "Union[Module, Type[Any], MethodType, FunctionType, TracebackType, FrameType, CodeType, Callable[..., Any]]" [arg-type] -src/rez/cli/selftest.py:0: error: Item "None" of "Optional[ModuleSpec]" has no attribute "loader" [union-attr] -src/rez/cli/selftest.py:0: error: Item "None" of "Union[Loader, Any, None]" has no attribute "load_module" [union-attr] +src/rez/utils/diff_packages.py:0: error: Argument 2 to "get_plugin_class" of "RezPluginManager" has incompatible type "str | None"; expected "str" [arg-type] +src/rez/utils/diff_packages.py:0: error: "type" has no attribute "export" [attr-defined] src/rez/cli/pkg-cache.py:0: error: Argument 1 to "append" of "list" has incompatible type "Callable[[Any], Any]"; expected "str" [arg-type] -src/rez/cli/pkg-cache.py:0: error: Argument 2 to "print_colored_columns" has incompatible type "List[List[Any]]"; expected "Sequence[Tuple[Any, ...]]" [arg-type] -src/rez/cli/memcache.py:0: error: "PackageFamily" has no attribute "name" [attr-defined] -src/rez/cli/memcache.py:0: error: "PackageFamily" has no attribute "name" [attr-defined] -src/rez/cli/memcache.py:0: error: "PackageFamily" has no attribute "name" [attr-defined] -src/rez/cli/memcache.py:0: error: Argument 1 to "append" of "list" has incompatible type "Tuple[Any, str, str, str, str, str, str]"; expected "List[str]" [arg-type] -src/rez/cli/benchmark.py:0: error: List item 0 has incompatible type "Optional[str]"; expected "str" [list-item] -src/rez/cli/benchmark.py:0: error: Name "_opts" is not defined [name-defined] -src/rez/cli/benchmark.py:0: error: List item 0 has incompatible type "Optional[str]"; expected "str" [list-item] -src/rez/cli/benchmark.py:0: error: Name "_opts" is not defined [name-defined] -src/rez/cli/benchmark.py:0: error: Item "None" of "Optional[List[Variant]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/cli/benchmark.py:0: error: Argument 1 to "join" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rez/cli/benchmark.py:0: error: Argument 1 to "join" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rez/cli/benchmark.py:0: error: Argument 1 to "exists" has incompatible type "Optional[str]"; expected "Union[int, Union[str, bytes, PathLike[str], PathLike[bytes]]]" [arg-type] -src/rez/cli/benchmark.py:0: error: Argument 1 to "mkdir" has incompatible type "Optional[str]"; expected "Union[str, bytes, PathLike[str], PathLike[bytes]]" [arg-type] -src/rez/cli/benchmark.py:0: error: Argument 1 to "join" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rez/cli/benchmark.py:0: error: Name "_opts" is not defined [name-defined] -src/rez/cli/benchmark.py:0: error: Argument 1 to "join" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rez/cli/benchmark.py:0: error: Argument 1 to "join" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rez/cli/benchmark.py:0: error: Name "_opts" is not defined [name-defined] +src/rez/cli/pkg-cache.py:0: error: Argument 2 to "print_colored_columns" has incompatible type "list[list[Any]]"; expected "Sequence[tuple[Any, ...]]" [arg-type] +src/rez/cli/memcache.py:0: error: Argument 1 to "append" of "list" has incompatible type "tuple[Any, str, str, str, str, str, str]"; expected "list[str]" [arg-type] +src/rez/cli/benchmark.py:0: error: Name "_opts" is not defined; did you mean "opts"? [name-defined] +src/rez/cli/benchmark.py:0: error: List item 0 has incompatible type "str | None"; expected "str" [list-item] +src/rez/cli/benchmark.py:0: error: Name "_opts" is not defined; did you mean "opts"? [name-defined] +src/rez/cli/benchmark.py:0: error: Item "None" of "list[Variant] | None" has no attribute "__iter__" (not iterable) [union-attr] +src/rez/cli/benchmark.py:0: error: Argument 1 to "join" has incompatible type "str | None"; expected "str" [arg-type] +src/rez/cli/benchmark.py:0: error: Argument 1 to "join" has incompatible type "str | None"; expected "str" [arg-type] +src/rez/cli/benchmark.py:0: error: Argument 1 to "exists" has incompatible type "str | None"; expected "int | str | bytes | PathLike[str] | PathLike[bytes]" [arg-type] +src/rez/cli/benchmark.py:0: error: Argument 1 to "mkdir" has incompatible type "str | None"; expected "str | bytes | PathLike[str] | PathLike[bytes]" [arg-type] +src/rez/cli/benchmark.py:0: error: Argument 1 to "join" has incompatible type "str | None"; expected "str" [arg-type] +src/rez/cli/benchmark.py:0: error: Name "_opts" is not defined; did you mean "opts"? [name-defined] +src/rez/cli/benchmark.py:0: error: Argument 1 to "join" has incompatible type "str | None"; expected "str" [arg-type] +src/rez/cli/benchmark.py:0: error: Argument 1 to "join" has incompatible type "str | None"; expected "str" [arg-type] +src/rez/cli/benchmark.py:0: error: Name "_opts" is not defined; did you mean "opts"? [name-defined] src/rez/cli/_util.py:0: error: Module has no attribute "CTRL_C_EVENT" [attr-defined] -src/rez/cli/_complete_util.py:0: error: Incompatible types in assignment (expression has type "Generator[str, None, None]", variable has type "List[str]") [assignment] +src/rez/cli/_complete_util.py:0: error: Incompatible types in assignment (expression has type "Generator[str, None, None]", variable has type "list[str]") [assignment] +src/rez/cli/selftest.py:0: error: Argument 1 to "getfile" has incompatible type "FrameType | None"; expected Module | type[Any] | MethodType | FunctionType | TracebackType | FrameType | CodeType | Callable[..., Any] [arg-type] +src/rez/cli/selftest.py:0: error: Item "None" of "ModuleSpec | None" has no attribute "loader" [union-attr] +src/rez/cli/selftest.py:0: error: Item "None" of "Loader | Any | None" has no attribute "load_module" [union-attr] src/rezplugins/shell/zsh.py:0: error: Incompatible types in assignment (expression has type "None", base class "UnixShell" defined the type as "str") [assignment] -src/rezplugins/shell/bash.py:0: error: Return type "Tuple[bool, bool, bool, bool]" of "startup_capabilities" incompatible with return type "Tuple[Union[str, Literal[False], None], bool, bool, bool]" in supertype "SH" [override] -src/rezplugins/shell/bash.py:0: error: Return type "Tuple[bool, bool, bool, bool]" of "startup_capabilities" incompatible with return type "Tuple[Union[str, Literal[False], None], bool, bool, bool]" in supertype "Shell" [override] -src/rezplugins/build_process/local.py:0: error: Argument 1 to "join" of "str" has incompatible type "List[Optional[str]]"; expected "Iterable[str]" [arg-type] -src/rezplugins/build_process/local.py:0: error: Argument 1 to "pre_variant_install" of "PackageRepository" has incompatible type "Resource"; expected "VariantResource" [arg-type] -src/rezplugins/build_process/local.py:0: error: Value of type "Optional[Dict[str, Any]]" is not indexable [index] -src/rezplugins/build_process/local.py:0: error: Argument 1 to "get_repository" of "PackageRepositoryManager" has incompatible type "Optional[str]"; expected "str" [arg-type] -src/rezplugins/build_process/local.py:0: error: Argument 1 to "on_variant_install_cancelled" of "PackageRepository" has incompatible type "Resource"; expected "VariantResource" [arg-type] -src/rezplugins/build_process/local.py:0: error: Argument 1 to "on_variant_install_cancelled" of "PackageRepository" has incompatible type "Resource"; expected "VariantResource" [arg-type] -src/rez/pip.py:0: error: Item "None" of "Optional[Variant]" has no attribute "parent" [union-attr] -src/rez/cli/release.py:0: error: Name "raw_input" is not defined [name-defined] -src/rez/cli/interpret.py:0: error: Incompatible types in assignment (expression has type "Python", variable has type "Optional[Shell]") [assignment] -src/rez/cli/help.py:0: error: Item "None" of "Optional[Package]" has no attribute "uri" [union-attr] -src/rez/cli/help.py:0: error: Item "None" of "Optional[Package]" has no attribute "description" [union-attr] -src/rez/cli/help.py:0: error: Item "None" of "Optional[Package]" has no attribute "description" [union-attr] +src/rezplugins/shell/zsh.py:0: error: Incompatible types in assignment (expression has type "str | Literal[False] | None", variable has type "str | None") [assignment] +src/rezplugins/shell/bash.py:0: error: Return type "tuple[bool, bool, bool, bool]" of "startup_capabilities" incompatible with return type "tuple[str | Literal[False] | None, bool, bool, bool]" in supertype "rezplugins.shell.sh.SH" [override] +src/rezplugins/shell/bash.py:0: error: Return type "tuple[bool, bool, bool, bool]" of "startup_capabilities" incompatible with return type "tuple[str | Literal[False] | None, bool, bool, bool]" in supertype "rez.shells.Shell" [override] +src/rezplugins/shell/bash.py:0: error: Incompatible types in assignment (expression has type "bool", variable has type "str | None") [assignment] +src/rezplugins/build_process/local.py:0: error: Argument 1 to "join" of "str" has incompatible type "list[str | None]"; expected "Iterable[str]" [arg-type] +src/rezplugins/build_process/local.py:0: error: Argument 2 to "join" has incompatible type "str | None"; expected "str" [arg-type] +src/rezplugins/build_process/local.py:0: error: Argument 2 to "join" has incompatible type "str | None"; expected "str" [arg-type] +src/rez/pip.py:0: error: Item "None" of "Variant | None" has no attribute "parent" [union-attr] +src/rez/cli/interpret.py:0: error: Incompatible types in assignment (expression has type "Python", variable has type "Shell") [assignment] +src/rez/cli/help.py:0: error: Item "None" of "Package | None" has no attribute "uri" [union-attr] +src/rez/cli/help.py:0: error: Item "None" of "Package | None" has no attribute "description" [union-attr] +src/rez/cli/help.py:0: error: Item "None" of "Package | None" has no attribute "description" [union-attr] src/rez/cli/env.py:0: error: Function "graph" could always be true in boolean context [truthy-function] src/rez/cli/env.py:0: error: "Popen[Any]" object is not iterable [misc] src/rez/cli/env.py:0: error: Cannot determine type of "returncode" [has-type] -src/rez/cli/diff.py:0: error: Argument 1 to "diff_packages" has incompatible type "Optional[Package]"; expected "Package" [arg-type] -src/rez/cli/cp.py:0: error: Incompatible types in assignment (expression has type "Optional[Any]", variable has type "List[Any]") [assignment] -src/rez/cli/context.py:0: error: Item "None" of "Optional[List[Variant]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/cli/context.py:0: error: Item "None" of "Optional[List[Variant]]" has no attribute "__iter__" (not iterable) [union-attr] -src/rez/cli/bind.py:0: error: Argument 1 to "sorted" has incompatible type "dict_items[str, str]"; expected "Iterable[List[str]]" [arg-type] +src/rez/cli/diff.py:0: error: Argument 1 to "diff_packages" has incompatible type "Package | None"; expected "Package" [arg-type] +src/rez/cli/cp.py:0: error: Incompatible types in assignment (expression has type "Any | None", variable has type "list[Any]") [assignment] +src/rez/cli/context.py:0: error: Item "None" of "list[Variant] | None" has no attribute "__iter__" (not iterable) [union-attr] +src/rez/cli/context.py:0: error: Item "None" of "list[Variant] | None" has no attribute "__iter__" (not iterable) [union-attr] +src/rez/cli/complete.py:0: error: Incompatible types in assignment (expression has type "list[Any]", variable has type "Generator[Any, None, None]") [assignment] src/rez/bind/python.py:0: error: Name "this" is not defined [name-defined] src/rez/bind/_pymodule.py:0: error: Name "env" is not defined [name-defined] src/rez/bind/_pymodule.py:0: error: Name "env" is not defined [name-defined] diff --git a/mypyc-custom.sh b/mypyc-custom.sh new file mode 100755 index 0000000000..b92cf90f97 --- /dev/null +++ b/mypyc-custom.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -e + +#pip install ../mypy +pip uninstall -y rez +pip install --no-build-isolation -e . +python -m rez.cli._main selftest --debug diff --git a/mypyc-test.sh b/mypyc-test.sh new file mode 100755 index 0000000000..383f828952 --- /dev/null +++ b/mypyc-test.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +set -e + +builddir=./build + +rm -rf $builddir +_REZ_NO_KILLPG=1 python ./install.py -v $builddir + + +#$builddir/bin/rez/rez-selftest + +#_REZ_NO_KILLPG=1 ./build/bin/python -c "import rez.utils.resources; print(rez.utils.resources.__file__); " +#_REZ_NO_KILLPG=1 ./build/bin/python -c "import rez.solver; print(rez.solver.__file__)" +#_REZ_NO_KILLPG=1 ./build/bin/python -c "import rez.resolver; print(rez.resolver.__file__)" +#_REZ_NO_KILLPG=1 ./build/bin/python -c "import rez.packages; print(rez.packages.__file__)" +# ./build/bin/python -c "import rez.version._version; print(rez.version._version.__file__)" + +./build/bin/rez/rez-selftest + +# ./build/bin/rez/rez-benchmark --out ./out-solver-resolver + +# PYTHONPATH=src python -c "import rez.utils.data_utils;rez.utils.data_utils.write_all_dynamic_members()" + +# python -m rez.cli._main selftest \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7a025147e9..08f9ab5f2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,30 +1,101 @@ [build-system] -requires = ["setuptools"] +# Building rez with mypyc (the default; set REZ_MYPYC=0 to build pure-python) +# requires our fork of mypy, which carries fixes needed to compile rez. Those +# fixes are pending upstreaming; until they land, pin the fork so that an +# isolated build (pip install ., install.py, etc.) picks up the right mypy. +# +# Local testing against a working copy of the mypy fork: +# The pin below points at the *pushed* fork branch on GitHub, so an isolated +# build always fetches it from the network and ignores any local mypy changes. +# To build against a local checkout instead, either: +# (a) recommended - install the fork editable in your venv and build with +# --no-build-isolation, which uses that installed mypy and ignores the +# requirement below entirely (this is what ./mypyc-custom.sh does): +# pip install -e /path/to/mypy +# pip install --no-build-isolation -e . +# (b) for an isolated build, temporarily replace the requirement below with +# a local path (revert before committing): +# "mypy[mypyc] @ file:///path/to/mypy" +requires = ["setuptools", "mypy[mypyc] @ git+https://github.com/chadrik/mypy@rez-mypyc-fixes ; python_version >= '3.10'"] build-backend = "setuptools.build_meta" [tool.mypy] -python_version = "3.8" +# mypy 2.1 requires the type-check target to be 3.10+, so we can no longer +# target rez's lowest supported runtime (3.8). 3.10 is the closest mypy allows. +python_version = "3.10" files = ["src/rez/", "src/rezplugins/"] exclude = [ '.*/rez/data/.*', - '.*/rez/vendor/.*', '.*/rez/tests/.*', '.*/rez/utils/lint_helper.py', ] disable_error_code = ["var-annotated", "import-not-found"] check_untyped_defs = true # allow this for now: -allow_redefinition = true +#allow_redefinition = true follow_imports = "silent" +enable_incomplete_feature = ["InlineTypedDict"] + +[[tool.mypy.overrides]] +module = "rez.vendor.*" +ignore_errors = true + +[[tool.mypy.overrides]] +module = "rez.vendor.schema.schema" +ignore_errors = false + +[[tool.mypy.overrides]] +module = "rez.utils.formatting" +disallow_untyped_defs = true [[tool.mypy.overrides]] module = 'rez.utils.lint_helper' follow_imports = "skip" [[tool.mypy.overrides]] -module = "rez.version._version" +module = "rez.packages" +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = "rez.package_filter" +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = "rez.package_order" +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = "rez.package_repository" +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = "rez.package_resources" +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = "rez.solver" +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = "rez.resolved_context" +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = "rez.resolver" disallow_untyped_defs = true [[tool.mypy.overrides]] -module = "rez.version._requirement" +module = "rez.version.*" disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = "rez.utils.resources" +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = "rez.utils.scope" +disallow_untyped_defs = true + +[[tool.mypy.overrides]] +module = "rezplugins.package_repository.filesystem" +disallow_untyped_defs = true \ No newline at end of file diff --git a/setup.py b/setup.py index 58aeacca85..eafa4d465e 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,49 @@ def find_files(pattern, path=None, root="rez"): long_description = f.read() +# Compile with mypyc only when explicitly requested via REZ_MYPYC=1. The +# default (and REZ_MYPYC=0) is a pure python build, regardless of python +# version. Requesting a mypyc build on a python that mypy does not support +# (< 3.10) is a hard error rather than a silent fallback. +use_mypyc = os.environ.get("REZ_MYPYC") == "1" + +if use_mypyc and sys.version_info < (3, 10): + raise RuntimeError( + "Building rez with mypyc requires Python 3.10 or newer (mypy does " + "not support older versions). Use Python >= 3.10, or set " + "REZ_MYPYC=0 to build pure python." + ) + +if use_mypyc: + from mypyc.build import mypycify + ext_modules = mypycify( + [ + # "src/rez/package_filter.py", # errors at runtime calling cached_property.uncache: "attribute 'cost' of 'PackageFilter' objects is not writable" + "src/rez/solver.py", + # "src/rez/resolved_context.py", # error in copy(). need to rework from_dict() + "src/rez/resolver.py", + "src/rez/package_order.py", # split out PackageOrderList from this module due to inheriting from list + "src/rez/package_repository.py", + "src/rez/package_resources.py", # requires fixed version of mypyc + "src/rez/version/__init__.py", + "src/rez/version/_util.py", + "src/rez/version/_requirement.py", + "src/rez/version/_version.py", + "src/rez/utils/formatting.py", # includes a mixin, which should not be compiled + # "src/rez/utils/memcached.py", + "src/rez/utils/resources.py", + # "src/rez/utils/scope.py", # objects directly access/modify __dict__ + # "src/rez/vendor/schema/schema.py", + "src/rezplugins/package_repository/filesystem.py", # ConfigurationError at runtime with FileSystemPackageRepository plugin + ], + # only_compile_paths=["src/rez/solver.py"] + opt_level="3", + multi_file=True + ) +else: + ext_modules = [] + + setup( name="rez", version=_rez_version, @@ -68,7 +111,8 @@ def find_files(pattern, path=None, root="rez"): packages=find_packages('src', exclude=["build_utils", "build_utils.*", "tests"]), - install_requires=[], + install_requires=["mypy_extensions"], + ext_modules=ext_modules, package_data={ 'rez': ['utils/logging.conf'] + diff --git a/src/rez/__init__.py b/src/rez/__init__.py index 6f9ef77751..daed41fa1b 100644 --- a/src/rez/__init__.py +++ b/src/rez/__init__.py @@ -58,10 +58,7 @@ def callback(sig, frame) -> None: txt = ''.join(traceback.format_stack(frame)) print() print(txt) - else: - callback = None - if callback: signal.signal(signal.SIGUSR1, callback) # Register handler diff --git a/src/rez/build_process.py b/src/rez/build_process.py index c4f8e69ce4..ed00c8aa45 100644 --- a/src/rez/build_process.py +++ b/src/rez/build_process.py @@ -404,10 +404,15 @@ def get_changelog(self) -> str | None: previous_revision = None changelog = None - with self.repo_operation(): - changelog = self.vcs.get_changelog( - previous_revision, - max_revisions=config.max_package_changelog_revisions) + # self.vcs is Optional; with no vcs there is no changelog to compute. + # Both callers already treat a None changelog as "no changelog" (one + # only reaches here after a vcs-is-None early return, the other catches + # and nulls it), so skipping the lookup when there is no vcs is safe. + if self.vcs: + with self.repo_operation(): + changelog = self.vcs.get_changelog( + previous_revision, + max_revisions=config.max_package_changelog_revisions) return changelog diff --git a/src/rez/cli/_complete_util.py b/src/rez/cli/_complete_util.py index 140e103a61..9db374f109 100644 --- a/src/rez/cli/_complete_util.py +++ b/src/rez/cli/_complete_util.py @@ -96,9 +96,10 @@ def __call__(self, prefix, **kwargs): return [] matching_names = [] - names = (x for x in names if x.startswith(fileprefix)) for name in names: + if not name.startswith(fileprefix): + continue filepath = os.path.join(path, name) if os.path.isfile(filepath): if not self.files: diff --git a/src/rez/cli/benchmark.py b/src/rez/cli/benchmark.py index 266e70361b..bcbee36e39 100644 --- a/src/rez/cli/benchmark.py +++ b/src/rez/cli/benchmark.py @@ -49,6 +49,8 @@ def load_packages() -> None: """ from rez.packages import iter_package_families + assert pkg_repo_dir is not None + print("Warming package cache...") fams = list(iter_package_families(paths=[pkg_repo_dir])) diff --git a/src/rez/cli/bind.py b/src/rez/cli/bind.py index 7a9b64123c..7b1bd65214 100644 --- a/src/rez/cli/bind.py +++ b/src/rez/cli/bind.py @@ -54,9 +54,9 @@ def command(opts, parser, extra_arg_groups=None) -> None: if opts.list: d = get_bind_modules() - rows = [["PACKAGE", "BIND MODULE"], - ["-------", "-----------"]] - rows += sorted(d.items()) + rows = [("PACKAGE", "BIND MODULE"), + ("-------", "-----------")] + rows += sorted((k, v) for k, v in d.items()) print('\n'.join(columnise(rows))) return diff --git a/src/rez/cli/interpret.py b/src/rez/cli/interpret.py index 3e26fb0fd1..457121a64b 100644 --- a/src/rez/cli/interpret.py +++ b/src/rez/cli/interpret.py @@ -56,7 +56,6 @@ def command(opts, parser, extra_arg_groups=None) -> None: with open(filename) as f: code = f.read() - interp = None if opts.format is None: interp = create_shell() elif opts.format in ('dict', 'table'): diff --git a/src/rez/config.py b/src/rez/config.py index b1c5f67d61..d33965f0d7 100644 --- a/src/rez/config.py +++ b/src/rez/config.py @@ -6,8 +6,9 @@ from rez import __version__ from rez.utils.data_utils import AttrDictWrapper, RO_AttrDictWrapper, \ - convert_dicts, cached_property, cached_class_property, LazyAttributeMeta, \ + convert_dicts, cached_class_property, LazyAttributeMeta, \ deep_update, ModifyList, DelayLoad +from functools import cached_property from rez.utils.formatting import expandvars, expanduser from rez.utils.logging_ import get_debug_printer from rez.utils.scope import scoped_format diff --git a/src/rez/developer_package.py b/src/rez/developer_package.py index 7015f03ed8..77e6e597a3 100644 --- a/src/rez/developer_package.py +++ b/src/rez/developer_package.py @@ -124,14 +124,14 @@ def from_path(cls, path: str, format: FileFormat | None = None) -> DeveloperPack # py sourcefiles into the package installation package.includes = set() - def visit(d): + def visit(includes: set, d: dict) -> None: for k, v in d.items(): if isinstance(v, SourceCode): - package.includes |= (v.includes or set()) + includes |= (v.includes or set()) elif isinstance(v, dict): - visit(v) + visit(includes, v) - visit(data) + visit(package.includes, data) package._validate_includes() diff --git a/src/rez/package_bind.py b/src/rez/package_bind.py index a600637305..68aaf822f9 100644 --- a/src/rez/package_bind.py +++ b/src/rez/package_bind.py @@ -191,7 +191,7 @@ def _print_package_list(variants) -> None: packages = set([x.parent for x in variants]) packages = sorted(packages, key=lambda x: x.name) - rows = [["PACKAGE", "URI"], - ["-------", "---"]] + rows = [("PACKAGE", "URI"), + ("-------", "---")] rows += [(x.name, x.uri) for x in packages] print('\n'.join(columnise(rows))) diff --git a/src/rez/package_copy.py b/src/rez/package_copy.py index 15589b8a0e..d8299c149a 100644 --- a/src/rez/package_copy.py +++ b/src/rez/package_copy.py @@ -241,6 +241,8 @@ def finalize(): variant_resource=src_variant.resource, overrides=overrides_ ) + # not a dry run, so a resource must have been returned + assert dest_variant_resource is not None dest_variant = Variant(dest_variant_resource) diff --git a/src/rez/package_filter.py b/src/rez/package_filter.py index 8e50d07ab5..a9189b50ba 100644 --- a/src/rez/package_filter.py +++ b/src/rez/package_filter.py @@ -7,15 +7,16 @@ from rez.packages import iter_packages, Package from rez.exceptions import ConfigurationError from rez.config import config -from rez.utils.data_utils import cached_property, cached_class_property +from functools import cached_property from rez.version import VersionedObject, VersionRange, Requirement +from rez.utils.data_utils import cached_class_property from hashlib import sha1 from typing import Any, Iterator, Pattern, TYPE_CHECKING, ClassVar import fnmatch import re if TYPE_CHECKING: - from typing import Self + from typing_extensions import Self class PackageFilterBase(object): @@ -58,7 +59,6 @@ def to_pod(self) -> Any: """Convert to POD type, suitable for storing in an rxt file. Return type depends on subclass implementation - dict[str, list[str]]: """ raise NotImplementedError @@ -106,6 +106,7 @@ class PackageFilter(PackageFilterBase): excluded if it matches one or more exclusion rules, and does not match any inclusion rules. """ + def __init__(self) -> None: self._excludes: dict[str | None, list[Rule]] = {} self._includes: dict[str | None, list[Rule]] = {} @@ -158,10 +159,12 @@ def copy(self) -> PackageFilter: def __and__(self, other: PackageFilter) -> PackageFilter: """Combine two filters.""" result = self.copy() - for rule in other._excludes.values(): - result.add_exclusion(rule) - for rule in other._includes.values(): - result.add_inclusion(rule) + for rules in other._excludes.values(): + for rule in rules: + result.add_exclusion(rule) + for rules in other._includes.values(): + for rule in rules: + result.add_inclusion(rule) return result def __bool__(self) -> bool: @@ -218,14 +221,15 @@ def _add_rule(self, rules_dict: dict[str | None, list[Rule]], rule: Rule) -> Non family = rule.family() rules_ = rules_dict.get(family, []) rules_dict[family] = sorted(rules_ + [rule], key=lambda x: x.cost()) - cached_property.uncache(self, "cost") # type: ignore[attr-defined] + # invalidate the functools.cached_property cache entry + self.__dict__.pop("cost", None) def __str__(self) -> str: def sortkey(rule_items: tuple[str | None, list[Rule]]) -> tuple[str, list[Rule]]: family, rules = rule_items if family is None: return ("", rules) - return rule_items + return family, rules return str((sorted(self._excludes.items(), key=sortkey), sorted(self._includes.items(), key=sortkey))) @@ -237,6 +241,7 @@ class PackageFilterList(PackageFilterBase): A package is excluded by a filter list iff any filter within the list excludes it. """ + def __init__(self) -> None: self.filters: list[PackageFilter] = [] @@ -334,7 +339,8 @@ class Rule(object): """Base package filter rule""" #: Rule name - name = None + name: str + _family: str | None def match(self, package: Package) -> bool: """Apply the rule to the package. @@ -437,6 +443,9 @@ class RegexRuleBase(Rule): regex: Pattern[str] txt: str + def __init__(self, s: str) -> None: + pass + def match(self, package: Package) -> bool: return bool(self.regex.match(package.qualified_name)) diff --git a/src/rez/package_maker.py b/src/rez/package_maker.py index 98ac025505..295a7e73f2 100644 --- a/src/rez/package_maker.py +++ b/src/rez/package_maker.py @@ -136,6 +136,7 @@ def get_package(self) -> Package: # retrieve the package from the new repository family_resource = repo.get_package_family(self.name) + assert family_resource is not None it = repo.iter_packages(family_resource) package_resource = next(it) @@ -203,19 +204,20 @@ def make_package(name: str, path: str, # package = maker.get_package() - src_variants = [] # skip those variants that already exist if skip_existing: + variants: list[Variant] = [] for variant in package.iter_variants(): variant_ = variant.install(path, dry_run=True) if variant_ is None: - src_variants.append(variant) + variants.append(variant) else: maker.skipped_variants.append(variant_) if warn_on_skip: print_warning("Skipping installation: Package variant already " "exists: %s" % variant_.uri) + src_variants: Iterable[Variant] = variants else: src_variants = package.iter_variants() diff --git a/src/rez/package_order.py b/src/rez/package_order.py index d8c7555753..e640b9c6c1 100644 --- a/src/rez/package_order.py +++ b/src/rez/package_order.py @@ -6,10 +6,8 @@ from inspect import isclass from hashlib import sha1 -from typing import Any, Callable, Iterable, List, TYPE_CHECKING +from typing import Any, Callable, ClassVar, Iterable, TYPE_CHECKING -from rez.config import config -from rez.utils.data_utils import cached_class_property from rez.version import Version, VersionRange from rez.version._version import _Comparable, _ReversedComparable, _LowerBound, _UpperBound, _Bound from rez.packages import iter_packages, Package @@ -18,7 +16,8 @@ if TYPE_CHECKING: # this is not available in typing until 3.11, but due to __future__.annotations # we can use it without really importing it - from typing import Self + from typing_extensions import Self + from rez.package_order_list import PackageOrderList ALL_PACKAGES = "*" @@ -35,12 +34,16 @@ def __init__(self, self.fallback_comparable = fallback_comparable def __eq__(self, other: object) -> bool: + if not isinstance(other, FallbackComparable): + return NotImplemented try: return self.main_comparable == other.main_comparable except Exception: return self.fallback_comparable == other.fallback_comparable def __lt__(self, other: object) -> bool: + if not isinstance(other, FallbackComparable): + return NotImplemented try: return self.main_comparable < other.main_comparable except Exception: @@ -54,15 +57,32 @@ class PackageOrder(object): """Package reorderer base class.""" #: Orderer name - name = None + name: ClassVar[str] + _packages: list[str] - def __init__(self, packages: list[str] | None = None) -> None: + def __init__(self, packages: str | list[str] | None = None) -> None: """ Args: - packages: If not provided, PackageOrder applies to all packages. + packages: A package family name, or list of package family names. + If not provided, PackageOrder applies to all packages. """ - # TYPING: odd behavior where mypy disregards the property setter - self.packages = packages # type: ignore[assignment] + # Note: do not assign via the `packages` property setter here. mypyc + # enforces the property getter's return type (list[str]) on setter + # assignments at runtime, so passing None/str through the setter would + # raise TypeError in the compiled extension (specifically from compiled + # call sites — mypyc inserts a runtime cast at each assignment site; + # interpreted callers can assign None/str through the public setter fine). + self._packages = self._normalize_packages(packages) + + @staticmethod + def _normalize_packages(packages: str | list[str] | None) -> list[str]: + if packages is None: + # Apply to all packages + return [ALL_PACKAGES] + elif isinstance(packages, str): + return [packages] + else: + return sorted(packages) @property def packages(self) -> list[str]: @@ -75,14 +95,8 @@ def packages(self) -> list[str]: return self._packages @packages.setter - def packages(self, packages: str | Iterable[str] | None) -> None: - if packages is None: - # Apply to all packages - self._packages = [ALL_PACKAGES] - elif isinstance(packages, str): - self._packages = [packages] - else: - self._packages = sorted(packages) + def packages(self, packages: str | list[str] | None) -> None: + self._packages = self._normalize_packages(packages) def reorder(self, iterable: Iterable[Package], key: Callable[[Any], Package] | None = None) -> list[Package] | None: @@ -108,6 +122,7 @@ def reorder(self, iterable: Iterable[Package], """ key = key or (lambda x: x) package_name = self._get_package_name_from_iterable(iterable, key=key) + assert package_name is not None return sorted(iterable, key=lambda x: self.sort_key(package_name, key(x).version), reverse=True) @@ -192,10 +207,10 @@ def sha1(self) -> str: def __str__(self) -> str: raise NotImplementedError - def __eq__(self, other): + def __eq__(self, other: object) -> bool: return type(self) == type(other) and str(self) == str(other) # noqa: E721 - def __ne__(self, other) -> bool: + def __ne__(self, other: object) -> bool: return not self == other def __repr__(self) -> str: @@ -219,7 +234,7 @@ def sort_key_implementation(self, package_name: str, version: Version) -> Suppor def __str__(self) -> str: return "{}" - def __eq__(self, other): + def __eq__(self, other: object) -> bool: return type(self) == type(other) # noqa: E721 def to_pod(self) -> dict[str, Any]: @@ -245,7 +260,7 @@ class SortedOrder(PackageOrder): """ name = "sorted" - def __init__(self, descending: bool, packages: list[str] | None = None) -> None: + def __init__(self, descending: bool, packages: str | list[str] | None = None) -> None: super().__init__(packages) self.descending = descending @@ -265,7 +280,9 @@ def sort_key_implementation(self, package_name: str, version: Version) -> Suppor def __str__(self) -> str: return str(self.descending) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, SortedOrder): + return NotImplemented return ( # noqa: E721 type(self) == type(other) and self.descending == other.descending @@ -344,7 +361,9 @@ def __str__(self) -> str: items = sorted((x[0], str(x[1])) for x in self.order_dict.items()) return str((items, str(self.default_order))) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, PerFamilyOrder): + return NotImplemented return ( # noqa: E721 type(other) == type(self) and self.order_dict == other.order_dict @@ -420,7 +439,7 @@ class VersionSplitPackageOrder(PackageOrder): """ name = "version_split" - def __init__(self, first_version: Version, packages: list[str] | None = None) -> None: + def __init__(self, first_version: Version, packages: str | list[str] | None = None) -> None: """Create a reorderer. Args: @@ -436,7 +455,9 @@ def sort_key_implementation(self, package_name: str, version: Version) -> Suppor def __str__(self) -> str: return str(self.first_version) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, VersionSplitPackageOrder): + return NotImplemented return ( # noqa: E721 type(other) == type(self) and self.first_version == other.first_version @@ -508,7 +529,7 @@ class TimestampPackageOrder(PackageOrder): """ name = "soft_timestamp" - def __init__(self, timestamp: int, rank: int = 0, packages: list[str] | None = None) -> None: + def __init__(self, timestamp: int, rank: int = 0, packages: str | list[str] | None = None) -> None: """Create a reorderer. Args: @@ -523,8 +544,8 @@ def __init__(self, timestamp: int, rank: int = 0, packages: list[str] | None = N # dictionary mapping from package family to the first-version-after # the given timestamp - self._cached_first_after = {} - self._cached_sort_key = {} + self._cached_first_after: dict[str, Version | None] = {} + self._cached_sort_key: dict[tuple[str, str], SupportsLessThan] = {} def _get_first_after(self, package_family: str) -> Version | None: """Get the first package version that is after the timestamp""" @@ -539,7 +560,7 @@ def _calc_first_after(self, package_family: str) -> Version | None: descending = sorted(iter_packages(package_family), key=lambda p: p.version, reverse=True) - first_after = None + first_after: Version | None = None for i, package in enumerate(descending): if not package.timestamp: continue @@ -581,6 +602,7 @@ def _calc_sort_key(self, package_name: str, version: Version) -> SupportsLessTha return is_before, version if self.rank: + assert version.tokens is not None return (is_before, _ReversedComparable(version.trim(self.rank - 1)), version.tokens[self.rank - 1:]) @@ -599,7 +621,9 @@ def sort_key_implementation(self, package_name: str, version: Version) -> Suppor def __str__(self) -> str: return str((self.timestamp, self.rank)) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, TimestampPackageOrder): + return NotImplemented return ( # noqa: E721 type(other) == type(self) and self.timestamp == other.timestamp @@ -632,96 +656,13 @@ def from_pod(cls, data: dict[str, Any]) -> Self: ) -class PackageOrderList(List[PackageOrder]): - """A list of package orderer. - """ - - def __init__(self, *args, **kwargs) -> None: - super().__init__(*args, **kwargs) - self.by_package: dict[str, PackageOrder] = {} - self.dirty = True - - def to_pod(self) -> list[dict[str, Any]]: - return [to_pod(f) for f in self] - - @classmethod - def from_pod(cls, data: list[dict[str, Any]]) -> PackageOrderList: - flist = PackageOrderList() - for dict_ in data: - f = from_pod(dict_) - flist.append(f) - return flist - - @cached_class_property - def singleton(cls) -> PackageOrderList: - """Filter list as configured by rezconfig.package_filter.""" - return cls.from_pod(config.package_orderers) - - @staticmethod - def _to_orderer(orderer: dict | PackageOrder) -> PackageOrder: - if isinstance(orderer, dict): - orderer = from_pod(orderer) - return orderer - - def refresh(self) -> None: - """Update the internal order-by-package mapping""" - self.by_package = {} - for orderer in self: - orderer = self._to_orderer(orderer) - for package in orderer.packages: - # We allow duplicates (so we can have hierarchical configs, - # which can override each other) - earlier orderers win - if package in self.by_package: - continue - self.by_package[package] = orderer - - if not TYPE_CHECKING: - # Since this class inherits from list it's easier to rely on the type hints coming from - # that base class than to redefine them here, so we hide them by placing them behind - # not TYPE_CHECKING. - - def append(self, *args, **kwargs): - self.dirty = True - return super().append(*args, **kwargs) - - def extend(self, *args, **kwargs): - self.dirty = True - return super().extend(*args, **kwargs) - - def pop(self, *args, **kwargs): - self.dirty = True - return super().pop(*args, **kwargs) - - def remove(self, *args, **kwargs): - self.dirty = True - return super().remove(*args, **kwargs) - - def clear(self, *args, **kwargs): - self.dirty = True - return super().clear(*args, **kwargs) - - def insert(self, *args, **kwargs): - self.dirty = True - return super().insert(*args, **kwargs) - - def get(self, key: str, default: PackageOrder | None = None) -> PackageOrder | None: - """ - Get an orderer that sorts a package by name. - """ - if self.dirty: - self.refresh() - self.dirty = False - result = self.by_package.get(key, default) - return result - - def to_pod(orderer: PackageOrder) -> dict: data = {"type": orderer.name} data.update(orderer.to_pod()) return data -def from_pod(data: dict[str, Any]) -> PackageOrder: +def from_pod(data: dict[str, Any] | tuple[str, dict[str, Any]]) -> PackageOrder: if isinstance(data, dict): cls_name = data["type"] data = data.copy() @@ -738,6 +679,9 @@ def from_pod(data: dict[str, Any]) -> PackageOrder: def get_orderer(package_name: str, orderers: PackageOrderList | dict[str, PackageOrder] | None = None) -> PackageOrder: if orderers is None: + # import here to avoid a circular import (package_order_list imports + # from this module) + from rez.package_order_list import PackageOrderList orderers = PackageOrderList.singleton orderer = orderers.get(package_name) if orderer is None: diff --git a/src/rez/package_order_list.py b/src/rez/package_order_list.py new file mode 100644 index 0000000000..3e7d58ea61 --- /dev/null +++ b/src/rez/package_order_list.py @@ -0,0 +1,94 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the Rez Project + + +from __future__ import annotations + +from typing import Any, List, TYPE_CHECKING + +from rez.config import config +from rez.package_order import PackageOrder, from_pod, to_pod +from rez.utils.data_utils import cached_class_property + + +class PackageOrderList(List[PackageOrder]): + """A list of package orderer. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.by_package: dict[str, PackageOrder] = {} + self.dirty = True + + def to_pod(self) -> list[dict[str, Any]]: + return [to_pod(f) for f in self] + + @classmethod + def from_pod(cls, data: list[dict[str, Any]]) -> PackageOrderList: + flist = PackageOrderList() + for dict_ in data: + f = from_pod(dict_) + flist.append(f) + return flist + + @cached_class_property + def singleton(cls) -> PackageOrderList: + """Filter list as configured by rezconfig.package_filter.""" + return cls.from_pod(config.package_orderers) + + @staticmethod + def _to_orderer(orderer: dict | PackageOrder) -> PackageOrder: + if isinstance(orderer, dict): + orderer = from_pod(orderer) + return orderer + + def refresh(self) -> None: + """Update the internal order-by-package mapping""" + self.by_package = {} + for orderer in self: + orderer = self._to_orderer(orderer) + for package in orderer.packages: + # We allow duplicates (so we can have hierarchical configs, + # which can override each other) - earlier orderers win + if package in self.by_package: + continue + self.by_package[package] = orderer + + if not TYPE_CHECKING: + # Since this class inherits from list it's easier to rely on the type hints coming from + # that base class than to redefine them here, so we hide them by placing them behind + # not TYPE_CHECKING. + + def append(self, *args, **kwargs): + self.dirty = True + return super().append(*args, **kwargs) + + def extend(self, *args, **kwargs): + self.dirty = True + return super().extend(*args, **kwargs) + + def pop(self, *args, **kwargs): + self.dirty = True + return super().pop(*args, **kwargs) + + def remove(self, *args, **kwargs): + self.dirty = True + return super().remove(*args, **kwargs) + + def clear(self, *args, **kwargs): + self.dirty = True + return super().clear(*args, **kwargs) + + def insert(self, *args, **kwargs): + self.dirty = True + return super().insert(*args, **kwargs) + + def get(self, key: str, default: PackageOrder | None = None) -> PackageOrder | None: + """ + Get an orderer that sorts a package by name. + """ + if self.dirty: + self.refresh() + self.dirty = False + result = self.by_package.get(key, default) + return result diff --git a/src/rez/package_repository.py b/src/rez/package_repository.py index 1141626a73..bfd9a0d87a 100644 --- a/src/rez/package_repository.py +++ b/src/rez/package_repository.py @@ -4,24 +4,28 @@ from __future__ import annotations -from rez.utils.resources import ResourcePool, ResourceHandle -from rez.utils.data_utils import cached_property +from rez.package_resources import (PackageFamilyResource, PackageResourceHelper, VariantResourceHelper) +from rez.utils.resources import ResourcePool, ResourceHandle, Resource, ResourceT from rez.plugin_managers import plugin_manager from rez.config import config from rez.exceptions import ResourceError from contextlib import contextmanager +from functools import cached_property import threading import os.path import time -from typing import Any, Hashable, Iterator, TYPE_CHECKING +from typing import cast, Any, Hashable, Generic, Iterator, TypeVar, TYPE_CHECKING, overload + +from rez.utils._mypyc import mypyc_attr if TYPE_CHECKING: - from rez.package_resources import (PackageFamilyResource, PackageResource, PackageResourceHelper, - VariantResource, PackageRepositoryResource) - from rez.utils.resources import Resource from rez.version import Version from rezplugins.package_repository.memory import MemoryPackageRepository +VariantResourceHelperT = TypeVar("VariantResourceHelperT", bound=VariantResourceHelper) +PackageResourceHelperT = TypeVar("PackageResourceHelperT", bound=PackageResourceHelper) +PackageFamilyResourceT = TypeVar("PackageFamilyResourceT", bound=PackageFamilyResource) + def get_package_repository_types() -> list[str]: """Returns the available package repository implementations.""" @@ -39,13 +43,17 @@ def create_memory_package_repository(repository_data: dict) -> MemoryPackageRepo Returns: `PackageRepository` object. """ - cls_ = plugin_manager.get_plugin_class("package_repository", "memory") + from rezplugins.package_repository.memory import MemoryPackageRepository # noqa + cls_ = cast( + "type[MemoryPackageRepository]", + plugin_manager.get_plugin_class("package_repository", "memory")) return cls_.create_repository(repository_data) class PackageRepositoryGlobalStats(threading.local): """Gathers stats across package repositories. """ + def __init__(self) -> None: # the amount of time that has been spent loading package from , # repositories, since process start @@ -66,7 +74,8 @@ def package_loading(self) -> Iterator[None]: package_repo_stats = PackageRepositoryGlobalStats() -class PackageRepository(object): +@mypyc_attr(allow_interpreted_subclasses=True) +class PackageRepository(Generic[VariantResourceHelperT, PackageResourceHelperT, PackageFamilyResourceT]): """Base class for package repositories implemented in the package_repository plugin type. @@ -122,7 +131,7 @@ def uid(self) -> tuple: """ return self._uid() - def __eq__(self, other) -> bool: + def __eq__(self, other: object) -> bool: return ( isinstance(other, PackageRepository) and other.name() == self.name() @@ -141,7 +150,7 @@ def is_empty(self) -> bool: return True - def get_package_family(self, name: str) -> PackageFamilyResource | None: + def get_package_family(self, name: str) -> PackageFamilyResourceT | None: """Get a package family. Args: @@ -152,7 +161,7 @@ def get_package_family(self, name: str) -> PackageFamilyResource | None: """ raise NotImplementedError - def iter_package_families(self) -> Iterator[PackageFamilyResource]: + def iter_package_families(self) -> Iterator[PackageFamilyResourceT]: """Iterate over the package families in the repository, in no particular order. @@ -161,7 +170,7 @@ def iter_package_families(self) -> Iterator[PackageFamilyResource]: """ raise NotImplementedError - def iter_packages(self, package_family_resource: PackageFamilyResource) -> Iterator[PackageResource]: + def iter_packages(self, package_family_resource: PackageFamilyResourceT) -> Iterator[PackageResourceHelperT]: """Iterate over the packages within the given family, in no particular order. @@ -173,7 +182,7 @@ def iter_packages(self, package_family_resource: PackageFamilyResource) -> Itera """ raise NotImplementedError - def iter_variants(self, package_resource: PackageResource) -> Iterator[VariantResource]: + def iter_variants(self, package_resource: PackageResourceHelperT) -> Iterator[VariantResourceHelperT]: """Iterate over the variants within the given package. Args: @@ -184,7 +193,7 @@ def iter_variants(self, package_resource: PackageResource) -> Iterator[VariantRe """ raise NotImplementedError - def get_package(self, name: str, version: Version) -> PackageResourceHelper | None: + def get_package(self, name: str, version: Version) -> PackageResourceHelperT | None: """Get a package. Args: @@ -204,7 +213,7 @@ def get_package(self, name: str, version: Version) -> PackageResourceHelper | No return None - def get_package_from_uri(self, uri: str) -> PackageResource | None: + def get_package_from_uri(self, uri: str) -> PackageResourceHelperT | None: """Get a package given its URI. Args: @@ -216,7 +225,7 @@ def get_package_from_uri(self, uri: str) -> PackageResource | None: """ return None - def get_variant_from_uri(self, uri: str) -> VariantResource | None: + def get_variant_from_uri(self, uri: str) -> VariantResourceHelperT | None: """Get a variant given its URI. Args: @@ -306,7 +315,7 @@ def remove_ignored_since(self, days: int, dry_run: bool = False, """ raise NotImplementedError - def pre_variant_install(self, variant_resource: VariantResource) -> None: + def pre_variant_install(self, variant_resource: VariantResourceHelper) -> None: """Called before a variant is installed. If any directories are created on disk for the variant to install into, @@ -315,9 +324,8 @@ def pre_variant_install(self, variant_resource: VariantResource) -> None: Note that it is the responsibility of the `BuildProcess` to call this function at the appropriate time. """ - pass - def on_variant_install_cancelled(self, variant_resource: VariantResource) -> None: + def on_variant_install_cancelled(self, variant_resource: VariantResourceHelper) -> None: """Called when a variant installation is cancelled. This is called after `pre_variant_install`, but before `install_variant`, @@ -330,12 +338,11 @@ def on_variant_install_cancelled(self, variant_resource: VariantResource) -> Non Note that it is the responsibility of the `BuildProcess` to call this function at the appropriate time. """ - pass def install_variant(self, - variant_resource: VariantResource, + variant_resource: VariantResourceHelper, dry_run: bool = False, - overrides: dict[str, Any] | None = None) -> VariantResource: + overrides: dict[str, Any] | None = None) -> VariantResourceHelperT | None: """Install a variant into this repository. Use this function to install a variant from some other package repository @@ -357,7 +364,7 @@ def install_variant(self, """ raise NotImplementedError - def get_equivalent_variant(self, variant_resource: VariantResource) -> VariantResource: + def get_equivalent_variant(self, variant_resource: VariantResourceHelper) -> VariantResourceHelperT | None: """Find a variant in this repository that is equivalent to that given. A variant is equivalent to another if it belongs to a package of the @@ -376,7 +383,7 @@ def get_equivalent_variant(self, variant_resource: VariantResource) -> VariantRe """ return self.install_variant(variant_resource, dry_run=True) - def get_parent_package_family(self, package_resource: PackageResourceHelper) -> PackageFamilyResource: + def get_parent_package_family(self, package_resource: PackageResourceHelperT) -> PackageFamilyResourceT: """Get the parent package family of the given package. Args: @@ -387,7 +394,7 @@ def get_parent_package_family(self, package_resource: PackageResourceHelper) -> """ raise NotImplementedError - def get_parent_package(self, variant_resource: VariantResource) -> PackageRepositoryResource: + def get_parent_package(self, variant_resource: VariantResourceHelperT) -> PackageResourceHelperT: """Get the parent package of the given variant. Args: @@ -398,7 +405,7 @@ def get_parent_package(self, variant_resource: VariantResource) -> PackageReposi """ raise NotImplementedError - def get_variant_state_handle(self, variant_resource: PackageResource + def get_variant_state_handle(self, variant_resource: VariantResourceHelperT ) -> Hashable | None: """Get a value that indicates the state of the variant. @@ -415,7 +422,7 @@ def get_variant_state_handle(self, variant_resource: PackageResource """ return None - def get_last_release_time(self, package_family_resource: PackageFamilyResource + def get_last_release_time(self, package_family_resource: PackageFamilyResourceT ) -> int: """Get the last time a package was added to the given family. @@ -454,6 +461,14 @@ def make_resource_handle(self, resource_key: str, **variables: Any) -> ResourceH variables = resource_cls.normalize_variables(variables) return ResourceHandle(resource_key, variables) + @overload + def get_resource(self, resource_key: type[ResourceT], **variables: Any) -> ResourceT: + pass + + @overload + def get_resource(self, resource_key: str, **variables: Any) -> Resource: + pass + def get_resource(self, resource_key: str | type[Resource], **variables: Any) -> Resource: """Get a resource. @@ -467,6 +482,8 @@ def get_resource(self, resource_key: str | type[Resource], **variables: Any) -> Returns: `PackageRepositoryResource` instance. """ + if isinstance(resource_key, type) and issubclass(resource_key, Resource): + resource_key = cast(str, getattr(resource_key, "key")) handle = self.make_resource_handle(resource_key, **variables) return self.get_resource_from_handle(handle, verify_repo=False) @@ -478,7 +495,7 @@ def get_resource_from_handle(self, resource_handle: ResourceHandle, resource_handle (`ResourceHandle`): Handle of the resource. Returns: - `PackageRepositoryResource` instance. + `Resource` instance. """ if verify_repo: # we could fix the handle at this point, but handles should @@ -498,7 +515,7 @@ def get_resource_from_handle(self, resource_handle: ResourceHandle, self.location)) resource = self.pool.get_resource_from_handle(resource_handle) - resource._repository = self + setattr(resource, "_repository", self) return resource def get_package_payload_path(self, package_name: str, package_version: str | Version | None = None) -> str: @@ -534,6 +551,7 @@ class PackageRepositoryManager(object): Manages retrieval of resources (packages and variants) from `PackageRepository` instances, and caches these resources in a resource pool. """ + def __init__(self, resource_pool: ResourcePool | None = None) -> None: """Create a package repo manager. @@ -567,9 +585,10 @@ def get_repository(self, path: str) -> PackageRepository: # normalise repo path parts = path.split('@', 1) if len(parts) == 1: - parts = ("filesystem", parts[0]) + repo_type, location = ("filesystem", parts[0]) + else: + repo_type, location = parts - repo_type, location = parts if repo_type == "filesystem": # choice of abspath here vs realpath is deliberate. Realpath gives # canonical path, which can be a problem if two studios are sharing @@ -637,7 +656,7 @@ def get_resource_from_handle(self, resource_handle: ResourceHandle resource_handle (`ResourceHandle`): Handle of the resource. Returns: - `PackageRepositoryResource` instance. + `Resource` instance. """ repo_type = resource_handle.get("repository_type") location = resource_handle.get("location") diff --git a/src/rez/package_resources.py b/src/rez/package_resources.py index 268b8c1800..7108ac459e 100644 --- a/src/rez/package_resources.py +++ b/src/rez/package_resources.py @@ -8,25 +8,31 @@ from rez.utils.schema import Required, schema_keys, extensible_schema_dict from rez.utils.logging_ import print_warning from rez.utils.sourcecode import SourceCode -from rez.utils.data_utils import cached_property, AttributeForwardMeta, \ - LazyAttributeMeta +from functools import cached_property from rez.utils.filesystem import find_matching_symlink from rez.utils.formatting import PackageRequest from rez.exceptions import PackageMetadataError, ResourceError from rez.config import config, Config, create_config -from rez.version import Requirement, Version +from rez.version import Version from rez.vendor.schema.schema import Schema, SchemaError, Optional, Or, And, Use from textwrap import dedent import os.path from abc import abstractmethod from hashlib import sha1 -from typing import Any, Iterable, Iterator, TYPE_CHECKING -from types import FunctionType, MethodType +from typing import cast, Any, Callable, Iterable, Iterator, Generic, TypeVar, TYPE_CHECKING, \ + ClassVar + +from rez.utils._mypyc import mypyc_attr if TYPE_CHECKING: - from rez.packages import Variant + import typing + from rez.package_repository import PackageRepository + +VariantResourceHelperT = TypeVar("VariantResourceHelperT", bound="VariantResourceHelper") +PackageResourceHelperT = TypeVar("PackageResourceHelperT", bound="PackageResourceHelper") +PackageRepositoryT = TypeVar("PackageRepositoryT", bound="PackageRepository") # package attributes created at release time package_release_keys = ( @@ -67,7 +73,7 @@ _is_late = And(SourceCode, lambda x: hasattr(x, "_late")) -def late_bound(schema): +def late_bound(schema: Any) -> Any: return Or(SourceCode, schema) @@ -272,15 +278,14 @@ def late_bound(schema): # resource classes # ------------------------------------------------------------------------------ -class PackageRepositoryResource(Resource): +@mypyc_attr(allow_interpreted_subclasses=True) +class PackageRepositoryResource(Resource, Generic[PackageRepositoryT]): """Base class for all package-related resources. """ schema_error = PackageMetadataError - #: Type of package repository associated with this resource type. - repository_type: str @classmethod - def normalize_variables(cls, variables): + def normalize_variables(cls, variables: dict[str, Any]) -> dict[str, Any]: if "repository_type" not in variables or "location" not in \ variables: raise ResourceError("%s resources require a repository_type and " @@ -288,20 +293,32 @@ def normalize_variables(cls, variables): return super(PackageRepositoryResource, cls).normalize_variables( variables) - def __init__(self, variables=None) -> None: - super(PackageRepositoryResource, self).__init__(variables) + def __init__(self, variables: dict[str, Any] | None = None) -> None: + super().__init__(variables) + # all Resources that are acquired using PackageRepository.get_resource + # have this attribute added to them + self._repository: PackageRepositoryT | None = None + + @property + def repository(self) -> PackageRepositoryT: + assert self._repository is not None + return self._repository @cached_property def uri(self) -> str: return self._uri() @property - def location(self) -> str | None: - return self.get("location") + def location(self) -> str: + location = self.get("location") + assert location is not None + return location @property - def name(self) -> str | None: - return self.get("name") + def name(self) -> str: + name = self.get("name") + assert name is not None + return name def _uri(self) -> str: """Return a URI. @@ -312,15 +329,21 @@ def _uri(self) -> str: raise NotImplementedError -class PackageFamilyResource(PackageRepositoryResource): +@mypyc_attr(allow_interpreted_subclasses=True) +class PackageFamilyResource( + PackageRepositoryResource[PackageRepositoryT], + Generic[PackageRepositoryT, PackageResourceHelperT]): """A package family. A repository implementation's package family resource(s) must derive from this class. It must satisfy the schema `package_family_schema`. """ - pass + + def iter_packages(self) -> Iterator[PackageResourceHelperT]: + raise NotImplementedError +@mypyc_attr(allow_interpreted_subclasses=True) class PackageResource(PackageRepositoryResource): """A package. @@ -329,7 +352,7 @@ class PackageResource(PackageRepositoryResource): """ @classmethod - def normalize_variables(cls, variables): + def normalize_variables(cls, variables: dict[str, Any]) -> dict[str, Any]: """Make sure version is treated consistently """ # if the version is False, empty string, etc, throw it out @@ -343,6 +366,7 @@ def version(self) -> Version: return Version(ver_str) +@mypyc_attr(allow_interpreted_subclasses=True) class VariantResource(PackageResource): """A package variant. @@ -356,7 +380,7 @@ class VariantResource(PackageResource): @property @abstractmethod - def parent(self) -> PackageRepositoryResource: + def parent(self) -> PackageResourceHelper: raise NotImplementedError @property @@ -364,12 +388,12 @@ def index(self) -> int | None: return self.get("index", None) @cached_property - def root(self) -> str: + def root(self) -> str | None: """Return the 'root' path of the variant.""" return self._root() @cached_property - def subpath(self) -> str: + def subpath(self) -> str | None: """Return the variant's 'subpath' The subpath is the relative path the variant's payload should be stored @@ -379,11 +403,11 @@ def subpath(self) -> str: return self._subpath() @abstractmethod - def _root(self, ignore_shortlinks: bool = False): + def _root(self, ignore_shortlinks: bool = False) -> str | None: raise NotImplementedError @abstractmethod - def _subpath(self, ignore_shortlinks: bool = False): + def _subpath(self, ignore_shortlinks: bool = False) -> str | None: raise NotImplementedError @@ -394,16 +418,12 @@ def _subpath(self, ignore_shortlinks: bool = False): # they may help minimise the amount of code you need to write. # ------------------------------------------------------------------------------ -class PackageResourceHelper(PackageResource): +@mypyc_attr(allow_interpreted_subclasses=True) +class PackageResourceHelper(PackageResource, Generic[VariantResourceHelperT]): """PackageResource with some common functionality included. """ - variant_key = None - if TYPE_CHECKING: - # I think these attributes are provided dynamically be LazyAttributeMeta - _commands: list[str] | str | FunctionType | MethodType | SourceCode - _pre_commands: list[str] | str | FunctionType | MethodType | SourceCode - _post_commands: list[str] | str | FunctionType | MethodType | SourceCode - variants: list[Variant] + # the resource key for a VariantResourceHelper subclass + variant_key: ClassVar[str] @property @abstractmethod @@ -416,18 +436,18 @@ def parent(self) -> PackageRepositoryResource: raise NotImplementedError @cached_property - def commands(self) -> SourceCode: + def commands(self) -> SourceCode | None: return self._convert_to_rex(self._commands) @cached_property - def pre_commands(self) -> SourceCode: + def pre_commands(self) -> SourceCode | None: return self._convert_to_rex(self._pre_commands) @cached_property - def post_commands(self) -> SourceCode: + def post_commands(self) -> SourceCode | None: return self._convert_to_rex(self._post_commands) - def iter_variants(self) -> Iterator[VariantResourceHelper]: + def iter_variants(self) -> Iterator[VariantResourceHelperT]: num_variants = len(self.variants or []) if num_variants == 0: @@ -436,15 +456,15 @@ def iter_variants(self) -> Iterator[VariantResourceHelper]: indexes = range(num_variants) for index in indexes: - variant = self._repository.get_resource( + variant = self.repository.get_resource( self.variant_key, location=self.location, name=self.name, version=self.get("version"), index=index) - yield variant + yield cast(VariantResourceHelperT, variant) - def _convert_to_rex(self, commands: list[str] | str | FunctionType | MethodType | SourceCode) -> SourceCode: + def _convert_to_rex(self, commands: list[str] | str | None | Callable | SourceCode) -> SourceCode | None: if isinstance(commands, list): from rez.utils.backcompat import convert_old_commands @@ -462,12 +482,136 @@ def _convert_to_rex(self, commands: list[str] | str | FunctionType | MethodType else: return commands + # -- BEGIN AUTO-GENERATED METHODS -- + @cached_property + def authors(self) -> list[str] | None: + return self._get_item('authors', True) + + @cached_property + def _base(self) -> str | None: + return self._get_item('base', True) + + @cached_property + def build_requires(self) -> typing.Union[SourceCode, list[PackageRequest]] | None: + return self._get_item('build_requires', True) + + @cached_property + def cachable(self) -> typing.Union[SourceCode, typing.Union[None, bool]] | None: + return self._get_item('cachable', True) + + @cached_property + def changelog(self) -> str | None: + return self._get_item('changelog', True) + + @cached_property + def _commands(self) -> typing.Union[SourceCode, typing.Callable, str, list[str]] | None: + return self._get_item('commands', True) + + @cached_property + def config(self) -> Config | None: + return self._get_item('config', True) + + @cached_property + def description(self) -> str | None: + return self._get_item('description', True) + + @cached_property + def has_plugins(self) -> typing.Union[SourceCode, bool] | None: + return self._get_item('has_plugins', True) + + @cached_property + def hashed_variants(self) -> bool | None: + return self._get_item('hashed_variants', True) + + @cached_property + def help(self) -> typing.Union[SourceCode, typing.Union[str, list[list[str]]]] | None: + return self._get_item('help', True) + + @cached_property + def _name(self) -> str: + return self._get_item('name', False) + + @cached_property + def plugin_for(self) -> typing.Union[SourceCode, list[str]] | None: + return self._get_item('plugin_for', True) + + @cached_property + def _post_commands(self) -> typing.Union[SourceCode, typing.Callable, str, list[str]] | None: + return self._get_item('post_commands', True) + + @cached_property + def pre_build_commands(self) -> typing.Union[SourceCode, typing.Callable, str, list[str]] | None: + return self._get_item('pre_build_commands', True) + + @cached_property + def _pre_commands(self) -> typing.Union[SourceCode, typing.Callable, str, list[str]] | None: + return self._get_item('pre_commands', True) + + @cached_property + def pre_test_commands(self) -> typing.Union[SourceCode, typing.Callable, str, list[str]] | None: + return self._get_item('pre_test_commands', True) + + @cached_property + def previous_revision(self) -> object | None: + return self._get_item('previous_revision', True) + + @cached_property + def previous_version(self) -> Version | None: + return self._get_item('previous_version', True) + + @cached_property + def private_build_requires(self) -> typing.Union[SourceCode, list[PackageRequest]] | None: + return self._get_item('private_build_requires', True) + + @cached_property + def release_message(self) -> typing.Union[None, str] | None: + return self._get_item('release_message', True) + + @cached_property + def relocatable(self) -> typing.Union[SourceCode, typing.Union[None, bool]] | None: + return self._get_item('relocatable', True) + + @cached_property + def requires(self) -> typing.Union[SourceCode, list[PackageRequest]] | None: + return self._get_item('requires', True) + + @cached_property + def revision(self) -> object | None: + return self._get_item('revision', True) + + @cached_property + def tests(self) -> typing.Union[SourceCode, dict[str, typing.Union[typing.Union[str, list[str]], dict[str, typing.Any]]]] | None: # noqa: E501 + return self._get_item('tests', True) + + @cached_property + def timestamp(self) -> int | None: + return self._get_item('timestamp', True) + + @cached_property + def tools(self) -> typing.Union[SourceCode, list[str]] | None: + return self._get_item('tools', True) + + @cached_property + def uuid(self) -> str | None: + return self._get_item('uuid', True) + + @cached_property + def variants(self) -> list[list[PackageRequest]] | None: + return self._get_item('variants', True) + + @cached_property + def vcs(self) -> str | None: + return self._get_item('vcs', True) -class _Metas(AttributeForwardMeta, LazyAttributeMeta): - pass + @cached_property + def _version(self) -> Version | None: + return self._get_item('version', True) + + # -- END AUTO-GENERATED METHODS -- -class VariantResourceHelper(VariantResource, metaclass=_Metas): +@mypyc_attr(allow_interpreted_subclasses=True) +class VariantResourceHelper(VariantResource): """Helper class for implementing variants that inherit properties from their parent package. @@ -527,26 +671,262 @@ def _root(self, ignore_shortlinks: bool = False) -> str | None: return self.base else: subpath = self._subpath(ignore_shortlinks=ignore_shortlinks) + assert subpath is not None, "Will always be non-None if self.index is non-None" root = os.path.join(self.base, subpath) return root @cached_property - def variant_requires(self) -> list[Requirement]: + def variant_requires(self) -> list[PackageRequest]: index = self.index if index is None: return [] else: try: - return self.parent.variants[index] or [] + return self.parent.variants[index] or [] # type: ignore[index] # covered by TypeError except (IndexError, TypeError): raise ResourceError( "Unexpected error - variant %s cannot be found in its " "parent package %s" % (self.uri, self.parent.uri)) @property - def wrapped(self): # forward Package attributes onto ourself + def wrapped(self) -> PackageResourceHelper: # forward Package attributes onto ourself return self.parent - def _load(self): + def _load(self) -> None: # doesn't have its own data, forwards on from parent instead return None + + # -- BEGIN AUTO-GENERATED METHODS -- + @property + def authors(self) -> list[str] | None: + return self.wrapped.authors + + @property + def base(self) -> str | None: + return self.wrapped.base + + @property + def build_requires(self) -> typing.Union[SourceCode, list[PackageRequest]] | None: + return self.wrapped.build_requires + + @property + def cachable(self) -> typing.Union[SourceCode, typing.Union[None, bool]] | None: + return self.wrapped.cachable + + @property + def changelog(self) -> str | None: + return self.wrapped.changelog + + @property + def commands(self) -> typing.Union[SourceCode, typing.Callable, str, list[str]] | None: + return self.wrapped.commands + + @property + def config(self) -> Config | None: + return self.wrapped.config + + @property + def description(self) -> str | None: + return self.wrapped.description + + @property + def has_plugins(self) -> typing.Union[SourceCode, bool] | None: + return self.wrapped.has_plugins + + @property + def hashed_variants(self) -> bool | None: + return self.wrapped.hashed_variants + + @property + def help(self) -> typing.Union[SourceCode, typing.Union[str, list[list[str]]]] | None: + return self.wrapped.help + + @property + def plugin_for(self) -> typing.Union[SourceCode, list[str]] | None: + return self.wrapped.plugin_for + + @property + def post_commands(self) -> typing.Union[SourceCode, typing.Callable, str, list[str]] | None: + return self.wrapped.post_commands + + @property + def pre_build_commands(self) -> typing.Union[SourceCode, typing.Callable, str, list[str]] | None: + return self.wrapped.pre_build_commands + + @property + def pre_commands(self) -> typing.Union[SourceCode, typing.Callable, str, list[str]] | None: + return self.wrapped.pre_commands + + @property + def pre_test_commands(self) -> typing.Union[SourceCode, typing.Callable, str, list[str]] | None: + return self.wrapped.pre_test_commands + + @property + def previous_revision(self) -> object | None: + return self.wrapped.previous_revision + + @property + def previous_version(self) -> Version | None: + return self.wrapped.previous_version + + @property + def private_build_requires(self) -> typing.Union[SourceCode, list[PackageRequest]] | None: + return self.wrapped.private_build_requires + + @property + def release_message(self) -> typing.Union[None, str] | None: + return self.wrapped.release_message + + @property + def relocatable(self) -> typing.Union[SourceCode, typing.Union[None, bool]] | None: + return self.wrapped.relocatable + + @property + def requires(self) -> typing.Union[SourceCode, list[PackageRequest]] | None: + return self.wrapped.requires + + @property + def revision(self) -> object | None: + return self.wrapped.revision + + @property + def tests(self) -> typing.Union[SourceCode, dict[str, typing.Union[typing.Union[str, list[str]], dict[str, typing.Any]]]] | None: # noqa: E501 + return self.wrapped.tests + + @property + def timestamp(self) -> int | None: + return self.wrapped.timestamp + + @property + def tools(self) -> typing.Union[SourceCode, list[str]] | None: + return self.wrapped.tools + + @property + def uuid(self) -> str | None: + return self.wrapped.uuid + + @property + def vcs(self) -> str | None: + return self.wrapped.vcs + + @cached_property + def _authors(self) -> list[str] | None: + return self._get_item('authors', True) + + @cached_property + def _base(self) -> str | None: + return self._get_item('base', True) + + @cached_property + def _build_requires(self) -> typing.Union[SourceCode, list[PackageRequest]] | None: + return self._get_item('build_requires', True) + + @cached_property + def _cachable(self) -> typing.Union[SourceCode, typing.Union[None, bool]] | None: + return self._get_item('cachable', True) + + @cached_property + def _changelog(self) -> str | None: + return self._get_item('changelog', True) + + @cached_property + def _commands(self) -> SourceCode | None: + return self._get_item('commands', True) + + @cached_property + def _config(self) -> Config | None: + return self._get_item('config', True) + + @cached_property + def _description(self) -> str | None: + return self._get_item('description', True) + + @cached_property + def _has_plugins(self) -> typing.Union[SourceCode, bool] | None: + return self._get_item('has_plugins', True) + + @cached_property + def _hashed_variants(self) -> bool | None: + return self._get_item('hashed_variants', True) + + @cached_property + def _help(self) -> typing.Union[SourceCode, typing.Union[str, list[list[str]]]] | None: + return self._get_item('help', True) + + @cached_property + def _name(self) -> str: + return self._get_item('name', False) + + @cached_property + def _plugin_for(self) -> typing.Union[SourceCode, list[str]] | None: + return self._get_item('plugin_for', True) + + @cached_property + def _post_commands(self) -> SourceCode | None: + return self._get_item('post_commands', True) + + @cached_property + def _pre_build_commands(self) -> SourceCode | None: + return self._get_item('pre_build_commands', True) + + @cached_property + def _pre_commands(self) -> SourceCode | None: + return self._get_item('pre_commands', True) + + @cached_property + def _pre_test_commands(self) -> SourceCode | None: + return self._get_item('pre_test_commands', True) + + @cached_property + def _previous_revision(self) -> object | None: + return self._get_item('previous_revision', True) + + @cached_property + def _previous_version(self) -> Version | None: + return self._get_item('previous_version', True) + + @cached_property + def _private_build_requires(self) -> typing.Union[SourceCode, list[PackageRequest]] | None: + return self._get_item('private_build_requires', True) + + @cached_property + def _release_message(self) -> typing.Union[None, str] | None: + return self._get_item('release_message', True) + + @cached_property + def _relocatable(self) -> typing.Union[SourceCode, typing.Union[None, bool]] | None: + return self._get_item('relocatable', True) + + @cached_property + def _requires(self) -> typing.Union[SourceCode, list[PackageRequest]] | None: + return self._get_item('requires', True) + + @cached_property + def _revision(self) -> object | None: + return self._get_item('revision', True) + + @cached_property + def _tests(self) -> typing.Union[SourceCode, dict[str, typing.Union[typing.Union[str, list[str]], dict[str, typing.Any]]]] | None: # noqa: E501 + return self._get_item('tests', True) + + @cached_property + def _timestamp(self) -> int | None: + return self._get_item('timestamp', True) + + @cached_property + def _tools(self) -> typing.Union[SourceCode, list[str]] | None: + return self._get_item('tools', True) + + @cached_property + def _uuid(self) -> str | None: + return self._get_item('uuid', True) + + @cached_property + def _vcs(self) -> str | None: + return self._get_item('vcs', True) + + @cached_property + def _version(self) -> Version | None: + return self._get_item('version', True) + + # -- END AUTO-GENERATED METHODS -- diff --git a/src/rez/package_search.py b/src/rez/package_search.py index 831da03702..13eb2b9c2d 100644 --- a/src/rez/package_search.py +++ b/src/rez/package_search.py @@ -62,8 +62,8 @@ def get_reverse_dependency_tree(package_name: str, g.add_node(package_name) # build reverse lookup - it = iter_package_families(paths) - package_names = set(x.name for x in it) + famit = iter_package_families(paths) + package_names = set(x.name for x in famit) if package_name not in package_names: raise PackageFamilyNotFoundError("No such package family %r" % package_name) diff --git a/src/rez/package_test.py b/src/rez/package_test.py index adc72fe2c4..5a7ef9aa7f 100644 --- a/src/rez/package_test.py +++ b/src/rez/package_test.py @@ -49,6 +49,7 @@ class PackageTestRunner(object): Commands can also be a list - in this case, the test process is launched directly, rather than interpreted via a shell. """ + def __init__(self, package_request, use_current_env: bool = False, extra_package_requests=None, package_paths=None, stdout=None, stderr=None, verbose: int = 0, dry_run: bool = False, stop_on_fail: bool = False, @@ -129,11 +130,12 @@ def get_package(self): else: # find latest package within request - package = get_latest_package_from_string(str(self.package_request), - self.package_paths) - if package is None: + maybe_package = get_latest_package_from_string(str(self.package_request), + self.package_paths) + if maybe_package is None: raise PackageNotFoundError("Could not find package to test: %s" % str(self.package_request)) + package = maybe_package self.package = package return self.package diff --git a/src/rez/packages.py b/src/rez/packages.py index e969d0b9f5..4627d24f3c 100644 --- a/src/rez/packages.py +++ b/src/rez/packages.py @@ -5,18 +5,20 @@ from __future__ import annotations from rez.package_repository import package_repository_manager -from rez.package_resources import PackageFamilyResource, PackageResource, \ - VariantResource, package_family_schema, package_schema, variant_schema, \ - package_release_keys, late_requires_schema +from rez.package_resources import ( + PackageFamilyResource, PackageResource, PackageRepositoryResource, PackageResourceHelper, + VariantResource, VariantResourceHelper, package_family_schema, package_schema, variant_schema, + package_release_keys, late_requires_schema) from rez.package_serialise import dump_package_data from rez.utils import reraise from rez.utils.sourcecode import SourceCode -from rez.utils.data_utils import cached_property -from rez.utils.formatting import StringFormatMixin, StringFormatType +from functools import cached_property +from rez.utils.formatting import ObjectStringFormatter, StringFormatType from rez.utils.schema import schema_keys from rez.utils.resources import ResourceHandle, ResourceWrapper from rez.exceptions import PackageFamilyNotFoundError, ResourceError from rez.utils.typing import SupportsWrite +from rez.utils.resources import Resource from rez.version import Version, VersionRange from rez.version import VersionedObject from rez.serialise import FileFormat @@ -24,7 +26,8 @@ import os import sys -from typing import overload, Any, Iterator, TypeVar, TYPE_CHECKING +import typing +from typing import overload, Any, ClassVar, Iterator, TypeVar, TYPE_CHECKING if TYPE_CHECKING: from rez.config import Config @@ -32,21 +35,50 @@ from rez.version import Requirement from rez.package_repository import PackageRepository from rez.resolved_context import ResolvedContext - from rez.utils.resources import Resource + from rez.utils.formatting import PackageRequest T = TypeVar("T") PackageT = TypeVar("PackageT", bound="Package") +PackageRepositoryResourceT = TypeVar("PackageRepositoryResourceT", bound=PackageRepositoryResource) +PackageOrVariantResourceT = TypeVar("PackageOrVariantResourceT", "PackageResourceHelper", VariantResourceHelper) # ------------------------------------------------------------------------------ # package-related classes # ------------------------------------------------------------------------------ -class PackageRepositoryResourceWrapper(ResourceWrapper, StringFormatMixin): - format_expand = StringFormatType.unchanged +class PackageRepositoryResourceWrapper(ResourceWrapper[PackageRepositoryResourceT]): + + format_expand: ClassVar[StringFormatType] = StringFormatType.unchanged + format_pretty: ClassVar[bool] = True + + def format(self, s: str, pretty: bool | None = None, + expand: StringFormatType | None = None) -> str: + """Format a string. + + Args: + s (str): String to format, eg "hello {name}" + pretty (bool): If True, references to non-string attributes such as + lists are converted to basic form, with characters such as + brackets and parenthesis removed. If None, defaults to the + object's 'format_pretty' attribute. + expand (`StringFormatType`): Expansion mode. If None, will default + to the object's 'format_expand' attribute. + + Returns: + The formatting string. + """ + if pretty is None: + pretty = self.format_pretty + if expand is None: + expand = self.format_expand + + formatter = ObjectStringFormatter(self, pretty=pretty, expand=expand) + return formatter.format(s) def validated_data(self) -> dict: data = ResourceWrapper.validated_data(self) + assert data is not None, "Should never be None if self.schema exists" data = dict((k, v) for k, v in data.items() if v is not None) return data @@ -57,10 +89,10 @@ def repository(self) -> PackageRepository: Returns: `PackageRepository`. """ - return self.resource._repository + return self.resource.repository -class PackageFamily(PackageRepositoryResourceWrapper): +class PackageFamily(PackageRepositoryResourceWrapper[PackageFamilyResource]): """A package family. Note: @@ -82,8 +114,15 @@ def iter_packages(self) -> Iterator[Package]: for package in self.repository.iter_packages(self.resource): yield Package(package) + # -- BEGIN AUTO-GENERATED METHODS -- + @property + def name(self) -> typing.Any: + return self.wrapped.name -class PackageBaseResourceWrapper(PackageRepositoryResourceWrapper): + # -- END AUTO-GENERATED METHODS -- + + +class PackageBaseResourceWrapper(PackageRepositoryResourceWrapper[PackageOrVariantResourceT]): """Abstract base class for `Package` and `Variant`. """ late_bind_schemas = { @@ -92,7 +131,7 @@ class PackageBaseResourceWrapper(PackageRepositoryResourceWrapper): "private_build_requires": late_requires_schema, } - def __init__(self, resource: PackageResource | VariantResource, context: ResolvedContext | None = None) -> None: + def __init__(self, resource: PackageOrVariantResourceT, context: ResolvedContext | None = None) -> None: super(PackageBaseResourceWrapper, self).__init__(resource) self.context = context @@ -123,7 +162,7 @@ def is_local(self) -> bool: """Returns True if the package is in the local package repository""" local_repo = package_repository_manager.get_repository( self.config.local_packages_path) - return (self.resource._repository.uid == local_repo.uid) + return (self.resource.repository.uid == local_repo.uid) def print_info(self, buf: SupportsWrite | None = None, format_: FileFormat = FileFormat.yaml, skip_attributes: list[str] | None = None, include_release: bool = False) -> None: @@ -200,7 +239,7 @@ def _eval_late_binding(self, sourcecode: SourceCode[T]) -> T: return sourcecode.exec_(globals_=g) -class Package(PackageBaseResourceWrapper): +class Package(PackageBaseResourceWrapper[PackageResourceHelper]): """A package. Warning: @@ -217,7 +256,7 @@ class Package(PackageBaseResourceWrapper): #: funcs, where ``this`` may be a package or variant. is_variant = False - def __init__(self, resource: PackageResource, context: ResolvedContext | None = None) -> None: + def __init__(self, resource: PackageResourceHelper, context: ResolvedContext | None = None) -> None: _check_class(resource, PackageResource) super(Package, self).__init__(resource, context) @@ -229,6 +268,13 @@ def __getattr__(self, name: str) -> Any: else: raise AttributeError("Package instance has no attribute '%s'" % name) + @property + def data(self) -> dict[str, Any]: + # super type declares result optional + data = super().data + assert data is not None + return data + def arbitrary_keys(self) -> set[str]: """Get the arbitrary keys present in this package. @@ -338,8 +384,131 @@ def get_variant(self, index: int | None = None) -> Variant | None: return variant return None + # -- BEGIN AUTO-GENERATED METHODS -- + @property + def authors(self) -> list[str] | None: + return self._wrap_forwarded('authors', self.wrapped.authors) + + @property + def base(self) -> str | None: + return self._wrap_forwarded('base', self.wrapped.base) + + @property + def build_requires(self) -> typing.Union[SourceCode, list[PackageRequest]] | None: + return self._wrap_forwarded('build_requires', self.wrapped.build_requires) + + @property + def cachable(self) -> typing.Union[SourceCode, typing.Union[None, bool]] | None: + return self._wrap_forwarded('cachable', self.wrapped.cachable) + + @property + def changelog(self) -> str | None: + return self._wrap_forwarded('changelog', self.wrapped.changelog) + + @property + def commands(self) -> typing.Union[SourceCode, typing.Callable, str, list[str]] | None: + return self._wrap_forwarded('commands', self.wrapped.commands) + + @property + def description(self) -> str | None: + return self._wrap_forwarded('description', self.wrapped.description) + + @property + def has_plugins(self) -> typing.Union[SourceCode, bool] | None: + return self._wrap_forwarded('has_plugins', self.wrapped.has_plugins) + + @property + def hashed_variants(self) -> bool | None: + return self._wrap_forwarded('hashed_variants', self.wrapped.hashed_variants) + + @property + def help(self) -> typing.Union[SourceCode, typing.Union[str, list[list[str]]]] | None: + return self._wrap_forwarded('help', self.wrapped.help) + + @property + def name(self) -> typing.Any: + return self._wrap_forwarded('name', self.wrapped.name) + + @property + def plugin_for(self) -> typing.Union[SourceCode, list[str]] | None: + return self._wrap_forwarded('plugin_for', self.wrapped.plugin_for) + + @property + def post_commands(self) -> typing.Union[SourceCode, typing.Callable, str, list[str]] | None: + return self._wrap_forwarded('post_commands', self.wrapped.post_commands) + + @property + def pre_build_commands(self) -> typing.Union[SourceCode, typing.Callable, str, list[str]] | None: + return self._wrap_forwarded('pre_build_commands', self.wrapped.pre_build_commands) + + @property + def pre_commands(self) -> typing.Union[SourceCode, typing.Callable, str, list[str]] | None: + return self._wrap_forwarded('pre_commands', self.wrapped.pre_commands) + + @property + def pre_test_commands(self) -> typing.Union[SourceCode, typing.Callable, str, list[str]] | None: + return self._wrap_forwarded('pre_test_commands', self.wrapped.pre_test_commands) + + @property + def previous_revision(self) -> object | None: + return self._wrap_forwarded('previous_revision', self.wrapped.previous_revision) + + @property + def previous_version(self) -> Version | None: + return self._wrap_forwarded('previous_version', self.wrapped.previous_version) + + @property + def private_build_requires(self) -> typing.Union[SourceCode, list[PackageRequest]] | None: + return self._wrap_forwarded('private_build_requires', self.wrapped.private_build_requires) + + @property + def release_message(self) -> typing.Union[None, str] | None: + return self._wrap_forwarded('release_message', self.wrapped.release_message) + + @property + def relocatable(self) -> typing.Union[SourceCode, typing.Union[None, bool]] | None: + return self._wrap_forwarded('relocatable', self.wrapped.relocatable) + + @property + def requires(self) -> typing.Union[SourceCode, list[PackageRequest]] | None: + return self._wrap_forwarded('requires', self.wrapped.requires) + + @property + def revision(self) -> object | None: + return self._wrap_forwarded('revision', self.wrapped.revision) + + @property + def tests(self) -> typing.Union[SourceCode, dict[str, typing.Union[typing.Union[str, list[str]], dict[str, typing.Any]]]] | None: # noqa: E501 + return self._wrap_forwarded('tests', self.wrapped.tests) + + @property + def timestamp(self) -> int | None: + return self._wrap_forwarded('timestamp', self.wrapped.timestamp) + + @property + def tools(self) -> typing.Union[SourceCode, list[str]] | None: + return self._wrap_forwarded('tools', self.wrapped.tools) + + @property + def uuid(self) -> str | None: + return self._wrap_forwarded('uuid', self.wrapped.uuid) + + @property + def variants(self) -> list[list[PackageRequest]] | None: + return self._wrap_forwarded('variants', self.wrapped.variants) + + @property + def vcs(self) -> str | None: + return self._wrap_forwarded('vcs', self.wrapped.vcs) + + @property + def version(self) -> Version: + return self._wrap_forwarded('version', self.wrapped.version) + + # -- END AUTO-GENERATED METHODS -- -class Variant(PackageBaseResourceWrapper): + +class Variant(PackageBaseResourceWrapper[VariantResourceHelper]): """A package variant. Warning: @@ -355,7 +524,7 @@ class Variant(PackageBaseResourceWrapper): #: See :attr:`Package.is_variant`. is_variant = True - def __init__(self, resource: VariantResource, context: ResolvedContext | None = None, + def __init__(self, resource: VariantResourceHelper, context: ResolvedContext | None = None, parent: Package | None = None) -> None: _check_class(resource, VariantResource) super(Variant, self).__init__(resource, context) @@ -484,15 +653,143 @@ def install(self, path: str, dry_run: bool = False, return Variant(resource) @property - def _non_shortlinked_subpath(self) -> str: + def _non_shortlinked_subpath(self) -> str | None: return self.resource._subpath(ignore_shortlinks=True) + # -- BEGIN AUTO-GENERATED METHODS -- + @property + def authors(self) -> list[str] | None: + return self._wrap_forwarded('authors', self.wrapped.authors) + + @property + def base(self) -> str | None: + return self._wrap_forwarded('base', self.wrapped.base) + + @property + def build_requires(self) -> typing.Union[SourceCode, list[PackageRequest]] | None: + return self._wrap_forwarded('build_requires', self.wrapped.build_requires) + + @property + def cachable(self) -> typing.Union[SourceCode, typing.Union[None, bool]] | None: + return self._wrap_forwarded('cachable', self.wrapped.cachable) + + @property + def changelog(self) -> str | None: + return self._wrap_forwarded('changelog', self.wrapped.changelog) + + @property + def commands(self) -> SourceCode | None: + return self._wrap_forwarded('commands', self.wrapped.commands) + + @property + def description(self) -> str | None: + return self._wrap_forwarded('description', self.wrapped.description) + + @property + def has_plugins(self) -> typing.Union[SourceCode, bool] | None: + return self._wrap_forwarded('has_plugins', self.wrapped.has_plugins) + + @property + def hashed_variants(self) -> bool | None: + return self._wrap_forwarded('hashed_variants', self.wrapped.hashed_variants) + + @property + def help(self) -> typing.Union[SourceCode, typing.Union[str, list[list[str]]]] | None: + return self._wrap_forwarded('help', self.wrapped.help) + + @property + def index(self) -> typing.Any: + return self._wrap_forwarded('index', self.wrapped.index) + + @property + def name(self) -> typing.Any: + return self._wrap_forwarded('name', self.wrapped.name) + + @property + def plugin_for(self) -> typing.Union[SourceCode, list[str]] | None: + return self._wrap_forwarded('plugin_for', self.wrapped.plugin_for) + + @property + def post_commands(self) -> SourceCode | None: + return self._wrap_forwarded('post_commands', self.wrapped.post_commands) + + @property + def pre_build_commands(self) -> SourceCode | None: + return self._wrap_forwarded('pre_build_commands', self.wrapped.pre_build_commands) + + @property + def pre_commands(self) -> SourceCode | None: + return self._wrap_forwarded('pre_commands', self.wrapped.pre_commands) + + @property + def pre_test_commands(self) -> SourceCode | None: + return self._wrap_forwarded('pre_test_commands', self.wrapped.pre_test_commands) + + @property + def previous_revision(self) -> object | None: + return self._wrap_forwarded('previous_revision', self.wrapped.previous_revision) + + @property + def previous_version(self) -> Version | None: + return self._wrap_forwarded('previous_version', self.wrapped.previous_version) + + @property + def private_build_requires(self) -> typing.Union[SourceCode, list[PackageRequest]] | None: + return self._wrap_forwarded('private_build_requires', self.wrapped.private_build_requires) + + @property + def release_message(self) -> typing.Union[None, str] | None: + return self._wrap_forwarded('release_message', self.wrapped.release_message) + + @property + def relocatable(self) -> typing.Union[SourceCode, typing.Union[None, bool]] | None: + return self._wrap_forwarded('relocatable', self.wrapped.relocatable) + + @property + def revision(self) -> object | None: + return self._wrap_forwarded('revision', self.wrapped.revision) + + @property + def root(self) -> typing.Any: + return self._wrap_forwarded('root', self.wrapped.root) + + @property + def subpath(self) -> typing.Any: + return self._wrap_forwarded('subpath', self.wrapped.subpath) + + @property + def tests(self) -> typing.Union[SourceCode, dict[str, typing.Union[typing.Union[str, list[str]], dict[str, typing.Any]]]] | None: # noqa: E501 + return self._wrap_forwarded('tests', self.wrapped.tests) + + @property + def timestamp(self) -> int | None: + return self._wrap_forwarded('timestamp', self.wrapped.timestamp) + + @property + def tools(self) -> typing.Union[SourceCode, list[str]] | None: + return self._wrap_forwarded('tools', self.wrapped.tools) + + @property + def uuid(self) -> str | None: + return self._wrap_forwarded('uuid', self.wrapped.uuid) + + @property + def vcs(self) -> str | None: + return self._wrap_forwarded('vcs', self.wrapped.vcs) + + @property + def version(self) -> Version | None: + return self._wrap_forwarded('version', self.wrapped.version) + + # -- END AUTO-GENERATED METHODS -- + class PackageSearchPath(object): """A list of package repositories. For example, $REZ_PACKAGES_PATH refers to a list of repositories. """ + def __init__(self, packages_path: list[str]) -> None: """Create a package repository list. @@ -524,7 +821,7 @@ def __contains__(self, package: Package | Variant) -> bool: bool: True if the resource is in the list of repositories, False otherwise. """ - return (package.resource._repository.uid in self._repository_uids) + return (package.resource.repository.uid in self._repository_uids) @cached_property def _repository_uids(self) -> set[tuple[str, str]]: @@ -674,6 +971,7 @@ def get_package_from_handle(package_handle: ResourceHandle | dict) -> Package: if isinstance(package_handle, dict): package_handle = ResourceHandle.from_dict(package_handle) package_resource = package_repository_manager.get_resource_from_handle(package_handle) + assert isinstance(package_resource, PackageResourceHelper) package = Package(package_resource) return package @@ -750,6 +1048,7 @@ def get_variant(variant_handle: ResourceHandle | dict, variant_handle = ResourceHandle.from_dict(variant_handle) variant_resource = package_repository_manager.get_resource_from_handle(variant_handle) + assert isinstance(variant_resource, VariantResourceHelper) variant = Variant(variant_resource, context=context) return variant @@ -937,7 +1236,23 @@ def get_completions(prefix: str, paths: list[str] | None = None, family_only: bo return words -def get_latest_package(name, range_=None, paths=None, error=False): +@overload +def get_latest_package(name: str, *, range_: VersionRange | None = None, + paths: list[str] | None = None, + error: Literal[True] = True) -> Package: + pass + + +@overload +def get_latest_package(name: str, *, range_: VersionRange | None = None, + paths: list[str] | None = None, + error: Literal[False] | bool = False) -> Package | None: + pass + + +def get_latest_package(name: str, *, range_: VersionRange | None = None, + paths: list[str] | None = None, + error: bool = False) -> Package | None: """Get the latest package for a given package name. Args: @@ -949,6 +1264,14 @@ def get_latest_package(name, range_=None, paths=None, error=False): Returns: `Package` object, or None if no package is found. + + .. note:: + ``range_``, ``paths`` and ``error`` are keyword-only. This is a + breaking change from earlier releases where they could be passed + positionally. They were made keyword-only because ``error``'s value + determines the return type (``Package`` vs ``Package | None``), and + typing that via overloads for every positional/keyword permutation + would require an unwieldy number of overloads. Pass them by keyword. """ it = iter_packages(name, range_=range_, paths=paths) try: diff --git a/src/rez/plugin_managers.py b/src/rez/plugin_managers.py index f0fc0e4107..04d6d45807 100644 --- a/src/rez/plugin_managers.py +++ b/src/rez/plugin_managers.py @@ -7,14 +7,15 @@ """ from __future__ import annotations -from rez.config import config, expand_system_vars, _load_config_from_filepaths +from rez.config import config, expand_system_vars, _load_config_from_filepaths, Validatable from rez.utils.formatting import columnise from rez.utils.schema import dict_to_schema -from rez.utils.data_utils import LazySingleton, cached_property, deep_update +from rez.utils.data_utils import LazySingleton, deep_update +from functools import cached_property from rez.utils.logging_ import print_debug, print_warning from rez.exceptions import RezPluginError from zipimport import zipimporter -from typing import overload, Any, Literal, TypeVar, TYPE_CHECKING +from typing import overload, Any, Literal, TYPE_CHECKING import pkgutil import os.path import sys @@ -35,9 +36,6 @@ from rez.command import Command -T = TypeVar("T") - - # modified from pkgutil standard library: # this function is called from the __init__.py files of each plugin type inside # the 'rezplugins' package. @@ -97,7 +95,7 @@ def append_if_valid(dir_) -> None: def uncache_rezplugins_module_paths(instance=None) -> None: instance = instance or plugin_manager - cached_property.uncache(instance, "rezplugins_module_paths") # type: ignore[attr-defined] + instance.__dict__.pop("rezplugins_module_paths", None) class RezPluginType(object): @@ -279,7 +277,7 @@ def get_plugin_module(self, plugin_name: str) -> types.ModuleType: % (self.pretty_type_name, plugin_name)) @cached_property - def config_schema(self): + def config_schema(self) -> Validatable: """Returns the merged configuration data schema for this plugin type.""" from rez.config import _plugin_config_dict @@ -350,6 +348,7 @@ def register_plugin(): This is important because it ensures that rez's copy of 'rezplugins' is always found first. """ + def __init__(self) -> None: self._plugin_types: dict[str, LazySingleton[RezPluginType]] = {} @@ -440,6 +439,10 @@ def get_plugin_class(self, plugin_type: Literal["build_process"], plugin_name: s def get_plugin_class(self, plugin_type: Literal["command"], plugin_name: str) -> type[Command]: pass + @overload + def get_plugin_class(self, plugin_type: str, plugin_name: str) -> type: + pass + def get_plugin_class(self, plugin_type: str, plugin_name: str) -> type: """Return the class registered under the given plugin name.""" plugin = self._get_plugin_type(plugin_type) @@ -451,7 +454,7 @@ def get_plugin_module(self, plugin_type: str, plugin_name: str) -> types.ModuleT plugin = self._get_plugin_type(plugin_type) return plugin.get_plugin_module(plugin_name) - def get_plugin_config_data(self, plugin_type: str): + def get_plugin_config_data(self, plugin_type: str) -> dict: """Return the merged configuration data for the plugin type.""" plugin = self._get_plugin_type(plugin_type) return plugin.config_data diff --git a/src/rez/release_hook.py b/src/rez/release_hook.py index e9e7881350..2ac3ca371a 100644 --- a/src/rez/release_hook.py +++ b/src/rez/release_hook.py @@ -57,7 +57,7 @@ def __init__(self, source_path) -> None: self.source_path = source_path self.package = get_developer_package(source_path) self.type_settings = self.package.config.plugins.release_hook - self.settings = self.type_settings.get(self.name()) + self.settings = self.type_settings[self.name()] def pre_build(self, user, install_path, variants=None, release_message=None, changelog=None, previous_version=None, diff --git a/src/rez/resolved_context.py b/src/rez/resolved_context.py index 3b45ac50d1..e99422246f 100644 --- a/src/rez/resolved_context.py +++ b/src/rez/resolved_context.py @@ -27,7 +27,8 @@ from rez import package_order from rez.packages import get_variant, iter_packages, Package, Variant from rez.package_filter import PackageFilterList -from rez.package_order import PackageOrder, PackageOrderList +from rez.package_order import PackageOrder +from rez.package_order_list import PackageOrderList from rez.package_cache import PackageCache from rez.shells import create_shell from rez.exceptions import ResolvedContextError, PackageCommandError, \ @@ -44,8 +45,8 @@ from contextlib import contextmanager from functools import wraps from enum import Enum -from typing import Any, Callable, Iterable, Iterator, Mapping, NoReturn, Sequence, TypeVar, \ - TYPE_CHECKING, overload +from typing import cast, Any, Callable, Iterable, Iterator, Mapping, NoReturn, Sequence, TypeVar, \ + TYPE_CHECKING, overload, ClassVar import getpass import json import socket @@ -54,6 +55,7 @@ import sys import os import os.path +from rez.utils._mypyc import mypyc_attr if TYPE_CHECKING: from typing import Literal # not available in typing module until 3.8 @@ -104,9 +106,41 @@ class PatchLock(Enum): __order__ = "no_lock,lock_2,lock_3,lock_4,lock" - def __init__(self, description: str, rank: int) -> None: - self.description = description - self.rank = rank + @property + def description(self) -> str: + return self.value[0] + + @property + def rank(self) -> int: + return self.value[1] + + # def __init__(self, description: str, rank: int) -> None: + # self.description = description + # self.rank = rank + + +@contextmanager +def _detect_bundle(path: str) -> Iterator[None]: + bundle_path = None + base_dir = os.path.dirname(os.path.abspath(path)) + bundle_filepath = os.path.join(base_dir, "bundle.yaml") + + try: + if os.path.exists(bundle_filepath): + bundle_path = base_dir + except IOError: + pass + + try: + if bundle_path: + ResolvedContext.local.bundle_path = bundle_path + + yield + finally: + try: + delattr(ResolvedContext.local, "bundle_path") + except AttributeError: + pass def get_lock_request(name: str, @@ -151,6 +185,31 @@ def _check(self: ResolvedContext, *nargs: Any, **kwargs: Any) -> Any: return _check # type: ignore[return-value] +class _Callback(object): + def __init__(self, max_fails: int, time_limit: int, + callback: Callable[[SolverState], tuple[SolverCallbackReturn, str]] | None, + buf: SupportsWrite | None = None) -> None: + self.max_fails = max_fails + self.time_limit = time_limit + self.callback = callback + self.start_time = time.time() + self.buf = buf or sys.stdout + + def __call__(self, state: SolverState) -> tuple[SolverCallbackReturn, str]: + if self.max_fails != -1 and state.num_fails >= self.max_fails: + reason = ("fail limit reached: aborted after %d failures" + % state.num_fails) + return SolverCallbackReturn.fail, reason + if self.time_limit != -1: + secs = time.time() - self.start_time + if secs > self.time_limit: + return SolverCallbackReturn.abort, "time limit exceeded" + if self.callback: + return self.callback(state) + return SolverCallbackReturn.keep_going, '' + + +@mypyc_attr(serializable=True) class ResolvedContext(object): """A class that resolves, stores and spawns Rez environments. @@ -162,40 +221,17 @@ class ResolvedContext(object): command within a configured python namespace, without spawning a child shell. """ - serialize_version = (4, 9) - tmpdir_manager = TempDirs(config.context_tmpdir, prefix="rez_context_") - context_tracking_payload: dict[str, Any] | None = None - context_tracking_lock = threading.Lock() - package_cache_present = True - local = threading.local() - - class Callback(object): - def __init__(self, max_fails: int, time_limit: int, - callback: Callable[[SolverState], tuple[SolverCallbackReturn, str]] | None, - buf: SupportsWrite | None = None) -> None: - self.max_fails = max_fails - self.time_limit = time_limit - self.callback = callback - self.start_time = time.time() - self.buf = buf or sys.stdout - - def __call__(self, state: SolverState) -> tuple[SolverCallbackReturn, str]: - if self.max_fails != -1 and state.num_fails >= self.max_fails: - reason = ("fail limit reached: aborted after %d failures" - % state.num_fails) - return SolverCallbackReturn.fail, reason - if self.time_limit != -1: - secs = time.time() - self.start_time - if secs > self.time_limit: - return SolverCallbackReturn.abort, "time limit exceeded" - if self.callback: - return self.callback(state) - return SolverCallbackReturn.keep_going, '' + serialize_version: ClassVar[tuple[int, int]] = (4, 9) + tmpdir_manager: ClassVar[TempDirs] = TempDirs(config.context_tmpdir, prefix="rez_context_") + context_tracking_payload: ClassVar[dict[str, Any] | None] = None + context_tracking_lock: ClassVar[threading.Lock] = threading.Lock() + package_cache_present: ClassVar[bool] = True + local: ClassVar[threading.local] = threading.local() def __init__(self, package_requests: Iterable[str | Requirement], verbosity: int = 0, - timestamp: float | None = None, + timestamp: int | None = None, building: bool = False, testing: bool = False, caching: bool | None = None, @@ -337,10 +373,10 @@ def __init__(self, self.suite_context_name: str | None = None # perform the solve - callback_ = self.Callback(buf=buf, - max_fails=max_fails, - time_limit=time_limit, - callback=callback) + callback_ = _Callback(buf=buf, + max_fails=max_fails, + time_limit=time_limit, + callback=callback) def _package_load_callback(package: Package) -> None: if package_load_callback: @@ -368,15 +404,15 @@ def _package_load_callback(package: Package) -> None: # convert the results self.status_ = resolver.status - self.solve_time = resolver.solve_time - self.load_time = resolver.load_time + self.solve_time = self._solved_value(resolver.solve_time) + self.load_time = self._solved_value(resolver.load_time) self.failure_description = resolver.failure_description self.graph_ = resolver.graph self.from_cache = resolver.from_cache if self.status_ == ResolverStatus.solved: self._resolved_packages = [] - for variant in resolver.resolved_packages: + for variant in self._solved_value(resolver.resolved_packages): variant.set_context(self) self._resolved_packages.append(variant) @@ -394,12 +430,19 @@ def __str__(self) -> str: request = self.requested_packages(include_implicit=True) req_str = " ".join(str(x) for x in request) if self.status == ResolverStatus.solved: - res_str = " ".join(x.qualified_name for x in self._resolved_packages) + res_str = " ".join(x.qualified_name for x in self._solved_value(self._resolved_packages)) return "%s(%s ==> %s)" % (self.status.name, req_str, res_str) else: return "%s:%s(%s)" % (self.__class__.__name__, self.status.name, req_str) + def _solved_value(self, x: T | None) -> T: + # if self.status_ != ResolverStatus.solved or self.status_ != ResolverStatus.failed: + # raise ResolvedContextError("This should only be called when solve is complete") + if x is None: + raise ResolvedContextError("This value should not be None after the solve is complete") + return x + @property def success(self) -> bool: """True if the context has been solved, False otherwise.""" @@ -456,13 +499,16 @@ def set_load_path(self, path: str) -> None: """ self.load_path = path - def __eq__(self, other): + def __eq__(self, other: object) -> bool: """Equality test. Two contexts are considered equal if they have an equivalent request, and an equivalent resolve. Other details, such as timestamp, are not considered. """ + if not isinstance(other, ResolvedContext): + return NotImplemented + return ( isinstance(other, ResolvedContext) and other.requested_packages(True) == self.requested_packages(True) @@ -496,6 +542,7 @@ def get_resolved_package(self, name: str) -> Variant | None: def copy(self) -> ResolvedContext: """Returns a shallow copy of the context.""" + # return self.from_dict(self.to_dict()) import copy return copy.copy(self) @@ -530,15 +577,14 @@ def retargeted(self, package_paths: list[str], package_names: list[str] | None = retargeted_variants.append(src_variant) continue - found = None + dest_variant = None for pkg_repo in pkg_repos: dest_variant = pkg_repo.get_equivalent_variant(src_variant.resource) if dest_variant is not None: - found = True break - if not found: + if dest_variant is None: if skip_missing: retargeted_variants.append(src_variant) continue @@ -606,9 +652,8 @@ def get_patched_request(self, package_requests: list[PackageRequest] | None = No # assemble source request if strict: request: list[Requirement | PackageRequest] = [] - for variant in self.resolved_packages: - req = PackageRequest(variant.qualified_package_name) - request.append(req) + for variant in self._solved_value(self.resolved_packages): + request.append(PackageRequest(variant.qualified_package_name)) else: request = self.requested_packages()[:] @@ -651,26 +696,26 @@ def get_patched_request(self, package_requests: list[PackageRequest] | None = No request += request_ # add rank limiters - if not strict and rank > 1: + if package_requests and not strict and rank > 1: overrides = set(x.name for x in package_requests if not x.conflict) rank_limiters = [] - for variant in self.resolved_packages: + for variant in self._solved_value(self.resolved_packages): if variant.name not in overrides: if len(variant.version) >= rank: version = variant.version.trim(rank - 1) version = next(version) - req = "~%s<%s" % (variant.name, str(version)) - rank_limiters.append(req) - request += rank_limiters - - return request + req_str = "~%s<%s" % (variant.name, str(version)) + rank_limiters.append(req_str) + return request + rank_limiters + else: + return cast("list[Requirement | PackageRequest | str]", request) @overload - def graph(self, as_dot: Literal[True]) -> str | None: + def graph(self, *, as_dot: Literal[True]) -> str | None: pass @overload - def graph(self, as_dot: Literal[False] = False) -> digraph | None: + def graph(self, *, as_dot: Literal[False] = False) -> digraph | None: pass def graph(self, as_dot: bool = False) -> str | digraph | None: @@ -688,7 +733,7 @@ def graph(self, as_dot: bool = False) -> str | digraph | None: return None if not as_dot: - if self.graph_ is None: + if self.graph_ is None and self.graph_string is not None: # reads either dot format or our compact format self.graph_ = read_graph_from_string(self.graph_string) return self.graph_ @@ -702,11 +747,14 @@ def graph(self, as_dot: bool = False) -> str | digraph | None: # compact format. return self.graph_string + if not self.graph_: + return None + return write_dot(self.graph_) def save(self, path: str) -> None: """Save the resolved context to file.""" - with self._detect_bundle(path): + with _detect_bundle(path): with open(path, 'w') as f: self.write_to_buffer(f) @@ -748,7 +796,7 @@ def is_current(self) -> bool | None: @classmethod def load(cls, path: str) -> ResolvedContext: """Load a resolved context from file.""" - with cls._detect_bundle(path): + with _detect_bundle(path): with open(path) as f: context = cls.read_from_buffer(f, path) @@ -763,6 +811,7 @@ def read_from_buffer(cls, buf: SupportsRead, identifier_str: str | None = None) except Exception as e: cls._load_error(e, identifier_str) + # @_on_success def get_resolve_diff(self, other: ResolvedContext) -> dict: """Get the difference between the resolve in this context and another. @@ -804,8 +853,8 @@ def get_resolve_diff(self, other: ResolvedContext) -> dict: # FIXME: make this a TypedDict d: dict[str, Any] = {} - self_pkgs_ = set(x.parent for x in self._resolved_packages) - other_pkgs_ = set(x.parent for x in other._resolved_packages) + self_pkgs_ = set(x.parent for x in self._solved_value(self._resolved_packages)) + other_pkgs_ = set(x.parent for x in self._solved_value(other._resolved_packages)) self_pkgs = self_pkgs_ - other_pkgs_ other_pkgs = other_pkgs_ - self_pkgs_ if not (self_pkgs or other_pkgs): @@ -915,12 +964,12 @@ def _rt(t: float) -> str: _pr("requested packages:", heading) rows = [] colors = [] - for request in self._package_requests: - if request.name.startswith('.'): - rows.append((str(request), "(ephemeral)")) + for req in self._package_requests: + if req.name.startswith('.'): + rows.append((str(req), "(ephemeral)")) colors.append(ephemeral_color) else: - rows.append((str(request), "")) + rows.append((str(req), "")) colors.append(None) for request in self.implicit_packages: @@ -1124,16 +1173,17 @@ def get_dependency_graph(self, as_dot: bool = False) -> digraph | str: """ from rez.vendor.pygraph.classes.digraph import digraph + resolved_packages = self._solved_value(self._resolved_packages) # add nodes nodes = {} - for variant in self._resolved_packages: + for variant in resolved_packages: nodes[variant.name] = variant.qualified_package_name - for ephemeral in self._resolved_ephemerals: + for ephemeral in self._solved_value(self._resolved_ephemerals): nodes[ephemeral.name] = str(ephemeral) # add edges edges = set() - for variant in self._resolved_packages: + for variant in resolved_packages: nodes[variant.name] = variant.qualified_package_name for request in variant.get_requires(): if not request.conflict: @@ -1160,7 +1210,7 @@ def get_dependency_graph(self, as_dot: bool = False) -> digraph | str: def validate(self) -> None: """Validate the context.""" try: - for pkg in self.resolved_packages: + for pkg in self._solved_value(self.resolved_packages): pkg.validate_data() except RezError as e: raise ResolvedContextError("%s: %s" % (e.__class__.__name__, str(e))) @@ -1198,7 +1248,7 @@ def get_key(self, key: str, request_only: bool = False) -> dict[str, tuple[Varia requested_names = [x.name for x in self._package_requests if not x.conflict] - for pkg in self.resolved_packages: + for pkg in self._solved_value(self.resolved_packages): if (not request_only) or (pkg.name in requested_names): value = getattr(pkg, key) if value is not None: @@ -1570,7 +1620,7 @@ def get_resolve_as_exact_requests(self) -> list[PackageRequest]: def to_req(variant: Variant) -> PackageRequest: return PackageRequest(variant.parent.as_exact_requirement()) - return [to_req(r) for r in self.resolved_packages] + return [to_req(r) for r in self._solved_value(self.resolved_packages)] def to_dict(self, fields: list[str] | None = None) -> dict: """Convert context to dict containing only builtin types. @@ -1697,7 +1747,7 @@ def _print_version(value: Iterable[int]) -> str: if identifier_str: msg.append("in %s" % identifier_str) msg.append("was written by a newer version of Rez. The load may " - "fail (serialize version %d > %d)" + "fail (serialize version %s > %s)" % (_print_version(load_ver), _print_version(curr_ver))) print(' '.join(msg), file=sys.stderr) @@ -1827,7 +1877,7 @@ def _execute_bundle_post_actions_callback(self, executor: RexExecutor) -> None: if not self.load_path: return - with self._detect_bundle(self.load_path): + with _detect_bundle(self.load_path): bundle_dir = self._get_bundle_path() if not bundle_dir: @@ -1844,30 +1894,6 @@ def _execute_bundle_post_actions_callback(self, executor: RexExecutor) -> None: header_comment(executor, "bundle post-commands") executor.execute_code(rex_py) - @classmethod - @contextmanager - def _detect_bundle(cls, path: str) -> Iterator[None]: - bundle_path = None - base_dir = os.path.dirname(os.path.abspath(path)) - bundle_filepath = os.path.join(base_dir, "bundle.yaml") - - try: - if os.path.exists(bundle_filepath): - bundle_path = base_dir - except IOError: - pass - - try: - if bundle_path: - cls.local.bundle_path = bundle_path - - yield - finally: - try: - delattr(cls.local, "bundle_path") - except AttributeError: - pass - @classmethod def _get_bundle_path(cls) -> str | None: return getattr(cls.local, "bundle_path", None) @@ -1976,7 +2002,8 @@ def _del(value: object) -> bool: if cls.context_tracking_payload is None: cls.context_tracking_payload = data - def _track_context(self, context_data, action: str) -> None: + # @_on_success + def _track_context(self, context_data: dict, action: str) -> None: # create message payload data = { "action": action, @@ -1984,7 +2011,7 @@ def _track_context(self, context_data, action: str) -> None: } self._init_context_tracking_payload_base() - data.update(self.context_tracking_payload) + data.update(self._solved_value(self.context_tracking_payload)) # publish message routing_key = ( @@ -2202,7 +2229,7 @@ def normalized(path: str) -> str: header = "Error in %s in package %r:\n" % (attr, pkg.uri) if self.verbosity >= 2: msg = header + str(exc) - else: + elif isinstance(exc, SourceCodeError): msg = header + exc.short_msg raise PackageCommandError(msg) diff --git a/src/rez/resolver.py b/src/rez/resolver.py index 162bdb525e..696e5155ff 100644 --- a/src/rez/resolver.py +++ b/src/rez/resolver.py @@ -18,13 +18,15 @@ from typing import Any, Callable, Iterator, TypedDict, TYPE_CHECKING if TYPE_CHECKING: - from rez.package_order import PackageOrderList + from rez.package_order_list import PackageOrderList from rez.resolved_context import ResolvedContext from rez.utils.typing import SupportsWrite + from rez.utils.resources import ResourceHandle + from rez.vendor.pygraph.classes.digraph import digraph class SolverDict(TypedDict): - status: ResolverStatus + status: "ResolverStatus" graph: Any # digraph solve_time: float | None load_time: float | None @@ -43,8 +45,10 @@ class ResolverStatus(Enum): failed = ("The resolve is not possible.", ) aborted = ("The resolve was stopped by the user (via callback).", ) - def __init__(self, description) -> None: - self.description = description + @property + def description(self) -> str: + """A human readable description of what the state represents.""" + return self.value[0] class Resolver(object): @@ -53,6 +57,7 @@ class Resolver(object): The Resolver uses a combination of Solver(s) and cache(s) to resolve a package request as quickly as possible. """ + def __init__(self, context: ResolvedContext, package_requests: list[Requirement], @@ -263,7 +268,7 @@ def _delete_cache_entry(key: str) -> None: client.delete(key) self._print("Discarded entry: %r", key) - def _retrieve(timestamped: bool) -> tuple[str, tuple[SolverDict, dict, dict]]: + def _retrieve(timestamped: bool) -> tuple[str, Any | Client._Miss]: key = self._memcache_key(timestamped=timestamped) self._print("Retrieving memcache key: %r", key) with self._memcached_client() as client: @@ -279,7 +284,7 @@ def _packages_changed(key: str, data: tuple[SolverDict, dict, dict]) -> bool: new_state = variant_states.get(variant) if new_state is None: try: - repo = variant.resource._repository + repo = variant.resource.repository new_state = repo.get_variant_state_handle(variant.resource) except (IOError, OSError) as e: # if, ie a package file was deleted on disk, then @@ -326,6 +331,7 @@ def _timestamp_is_earlier(key: str, data: tuple[SolverDict, dict, dict]) -> bool if self.timestamp: if data: + assert not isinstance(data, Client._Miss) if _packages_changed(key, data) or _releases_since_solve(key, data): _delete_cache_entry(key) elif not _timestamp_is_earlier(key, data): @@ -334,6 +340,7 @@ def _timestamp_is_earlier(key: str, data: tuple[SolverDict, dict, dict]) -> bool key, data = _retrieve(True) if not data: return _miss() # type: ignore[func-returns-value] + assert not isinstance(data, Client._Miss) if _packages_changed(key, data): _delete_cache_entry(key) return _miss() # type: ignore[func-returns-value] @@ -342,6 +349,7 @@ def _timestamp_is_earlier(key: str, data: tuple[SolverDict, dict, dict]) -> bool else: if not data: return _miss() # type: ignore[func-returns-value] + assert not isinstance(data, Client._Miss) if _packages_changed(key, data) or _releases_since_solve(key, data): _delete_cache_entry(key) return _miss() # type: ignore[func-returns-value] @@ -377,6 +385,8 @@ def _set_cached_solve(self, solver_dict: SolverDict) -> None: release_times_dict = {} variant_states_dict = {} + assert self.resolved_packages_ is not None, \ + "self.resolved_packages_ is set in _set_result when status is 'solved'" for variant in self.resolved_packages_: time_ = get_last_release_time(variant.name, self.package_paths) @@ -391,7 +401,7 @@ def _set_cached_solve(self, solver_dict: SolverDict) -> None: releases_since_solve = True release_times_dict[variant.name] = time_ - repo = variant.resource._repository + repo = variant.resource.repository variant_states_dict[variant.name] = \ repo.get_variant_state_handle(variant.resource) @@ -443,7 +453,7 @@ def _solve(self) -> Solver: return solver def _set_result(self, solver_dict: SolverDict) -> None: - self.status_ = solver_dict.get("status") + self.status_ = solver_dict["status"] self.graph_ = solver_dict.get("graph") self.solve_time = solver_dict.get("solve_time") self.load_time = solver_dict.get("load_time") @@ -485,11 +495,13 @@ def _solver_to_dict(cls, solver: Solver) -> SolverDict: status_ = ResolverStatus.solved variant_handles = [] + assert solver.resolved_packages is not None, "should be set after solve is complete" for solver_variant in solver.resolved_packages: variant_handle_dict = solver_variant.handle variant_handles.append(variant_handle_dict) ephemerals = [] + assert solver.resolved_ephemerals is not None, "should be set after solve is complete" for ephemeral in solver.resolved_ephemerals: ephemerals.append(str(ephemeral)) diff --git a/src/rez/serialise.py b/src/rez/serialise.py index 7cb61113ea..26711d64c9 100644 --- a/src/rez/serialise.py +++ b/src/rez/serialise.py @@ -16,6 +16,7 @@ import os.path import threading from io import StringIO +from typing import Any from rez.package_resources import package_rex_keys from rez.utils.scope import ScopeContext @@ -106,7 +107,7 @@ def open_file_for_write(filepath, mode=None): file_cache[filepath] = cache_filepath -def load_from_file(filepath: str, format_=FileFormat.py, update_data_callback=None, +def load_from_file(filepath: str, format_: FileFormat = FileFormat.py, update_data_callback=None, disable_memcache: bool = False): """Load data from a file. @@ -143,7 +144,7 @@ def load_from_file(filepath: str, format_=FileFormat.py, update_data_callback=No update_data_callback=update_data_callback) -def _load_from_file__key(filepath, format_, update_data_callback): +def _load_from_file__key(filepath: str, format_: FileFormat, update_data_callback): st = os.stat(filepath) if update_data_callback is None: callback_key = 'None' @@ -162,7 +163,7 @@ def _load_from_file(filepath: str, format_, update_data_callback): return _load_file(filepath, format_, update_data_callback) -def _load_file(filepath: str, format_, update_data_callback, original_filepath=None): +def _load_file(filepath: str, format_: FileFormat, update_data_callback, original_filepath=None): load_func = load_functions[format_] if debug_print: @@ -221,7 +222,7 @@ def set_objects(objects): _set_objects.variables = {} -def load_py(stream, filepath: str = None): +def load_py(stream, filepath: str = None) -> dict[str, Any]: """Load python-formatted data from a stream. Args: @@ -234,7 +235,7 @@ def load_py(stream, filepath: str = None): return _load_py(stream, filepath=filepath) -def _load_py(stream, filepath: str = None): +def _load_py(stream, filepath: str = None) -> dict[str, Any]: scopes = ScopeContext() g = dict(scope=scopes, @@ -399,7 +400,7 @@ def _trim(value): return data -def load_yaml(stream, filepath: str = None): +def load_yaml(stream, filepath: str = None) -> dict[str, Any]: """Load yaml-formatted data from a stream. Args: @@ -428,7 +429,7 @@ def load_yaml(stream, filepath: str = None): raise e -def load_txt(stream, filepath: str = None): +def load_txt(stream, filepath: str = None) -> str: """Load text data from a stream. Args: diff --git a/src/rez/shells.py b/src/rez/shells.py index f949d91348..6c352572b5 100644 --- a/src/rez/shells.py +++ b/src/rez/shells.py @@ -26,7 +26,7 @@ import subprocess # this is not available in typing until 3.11, but due to __future__.annotations # we can use it without really importing it - from typing import Self + from typing_extensions import Self def get_shell_types() -> list[str]: @@ -50,7 +50,7 @@ def get_shell_class(shell: str | None = None) -> type[Shell]: if not shell: from rez.system import system shell = system.shell - + assert shell is not None from rez.plugin_managers import plugin_manager return plugin_manager.get_plugin_class("shell", shell) diff --git a/src/rez/solver.py b/src/rez/solver.py index 55656f9eee..6412519509 100644 --- a/src/rez/solver.py +++ b/src/rez/solver.py @@ -17,7 +17,7 @@ from rez.packages import iter_packages, Package, Variant from rez.package_repository import package_repo_stats from rez.utils.logging_ import print_debug -from rez.utils.data_utils import cached_property +from functools import cached_property from rez.vendor.pygraph.classes.digraph import digraph from rez.vendor.pygraph.algorithms.cycles import find_cycle from rez.vendor.pygraph.algorithms.accessibility import accessibility @@ -38,8 +38,9 @@ if TYPE_CHECKING: from rez.resolved_context import ResolvedContext from rez.package_filter import PackageFilterBase - from rez.package_order import PackageOrderList + from rez.package_order_list import PackageOrderList +from rez.utils._mypyc import mypyc_attr T = TypeVar("T") @@ -86,8 +87,10 @@ class SolverStatus(Enum): cyclic = ("The solve contains a cycle.", ) unsolved = ("The solve has started, but is not yet solved.", ) - def __init__(self, description): - self.description = description + @property + def description(self) -> str: + """A human readable description of what the state represents.""" + return self.value[0] class SolverCallbackReturn(Enum): @@ -192,6 +195,9 @@ def involved_requirements(self) -> list[Requirement]: return [req, self.dependency, self.conflicting_request] def __eq__(self, other: object) -> bool: + if not isinstance(other, Reduction): + return NotImplemented + return (self.name == other.name and self.version == other.version and self.variant_index == other.variant_index @@ -216,6 +222,9 @@ def __init__(self, dependency: Requirement, conflicting_request: Requirement) -> self.conflicting_request = conflicting_request def __eq__(self, other: object) -> bool: + if not isinstance(other, DependencyConflict): + return NotImplemented + return (self.dependency == other.dependency) \ and (self.conflicting_request == other.conflicting_request) @@ -246,7 +255,9 @@ def involved_requirements(self) -> list[Requirement]: def description(self) -> str: return "A package was completely reduced: %s" % str(self) - def __eq__(self, other: TotalReduction) -> bool: + def __eq__(self, other: object) -> bool: + if not isinstance(other, TotalReduction): + return NotImplemented return (self.reductions == other.reductions) def __str__(self) -> str: @@ -269,7 +280,9 @@ def involved_requirements(self) -> list[Requirement]: def description(self) -> str: return "The following package conflicts occurred: %s" % str(self) - def __eq__(self, other: DependencyConflicts) -> bool: + def __eq__(self, other: object) -> bool: + if not isinstance(other, DependencyConflicts): + return NotImplemented return (self.conflicts == other.conflicts) def __str__(self) -> str: @@ -292,7 +305,9 @@ def involved_requirements(self) -> list[Requirement]: def description(self) -> str: return "A cyclic dependency was detected: %s" % str(self) - def __eq__(self, other: Cycle) -> bool: + def __eq__(self, other: object) -> bool: + if not isinstance(other, Cycle): + return NotImplemented return (self.packages == other.packages) def __str__(self) -> str: @@ -319,7 +334,9 @@ def name(self) -> str: @property def version(self) -> Version: - return self.variant.version + version = self.variant.version + assert version is not None, "should not be None because PackageResource.version is not None" + return version @property def index(self) -> int | None: @@ -357,7 +374,9 @@ def conflict_request_fams(self) -> set[str]: def get(self, pkg_name: str) -> Requirement | None: return self.requires_list.get(pkg_name) - def __eq__(self, other: PackageVariant) -> bool: + def __eq__(self, other: object) -> bool: + if not isinstance(other, PackageVariant): + return NotImplemented return ( self.name == other.name and self.version == other.version @@ -368,7 +387,7 @@ def __lt__(self, other: PackageVariant) -> bool: return ( self.name < other.name and self.version < other.version - and self.index < other.index + and (self.index is not None and other.index is not None and self.index < other.index) ) def __str__(self) -> str: @@ -390,7 +409,9 @@ def __init__(self, package: Package, variants: list[PackageVariant], solver: Sol @property def version(self) -> Version: - return self.package.version + version = self.package.version + assert version is not None, "should not be None because PackageResource.version is not None" + return version def __len__(self) -> int: return len(self.variants) @@ -441,7 +462,7 @@ def key(variant: PackageVariant) -> tuple[SupportsLessThan, ...]: req = variant.requires_list.get(request.name) if req is not None: orderer = get_orderer(req.name, orderers=self.solver.package_orderers or {}) - range_key = orderer.sort_key(req.name, req.range) + range_key = orderer.sort_key(req.name, req.range_) requested_key.append((-i, range_key)) names.add(req.name) @@ -449,22 +470,22 @@ def key(variant: PackageVariant) -> tuple[SupportsLessThan, ...]: for request in variant.requires_list: if not request.conflict and request.name not in names: orderer = get_orderer(request.name, orderers=self.solver.package_orderers) - range_key = orderer.sort_key(request.name, request.range) + range_key = orderer.sort_key(request.name, request.range_) additional_key.append((range_key, request.name)) if (VariantSelectMode[config.variant_select_mode] == VariantSelectMode.version_priority): - k = (requested_key, - -len(additional_key), - additional_key, - variant.index) + return (requested_key, + -len(additional_key), + additional_key, + # None does not support proper sorting, so fall back to int + variant.index or -1) else: # VariantSelectMode.intersection_priority - k = (len(requested_key), - requested_key, - -len(additional_key), - additional_key, - variant.index) - - return k + return (len(requested_key), + requested_key, + -len(additional_key), + additional_key, + # None does not support proper sorting, so fall back to int + variant.index or -1) self.variants.sort(key=key, reverse=True) self.sorted = True @@ -575,6 +596,7 @@ def __str__(self) -> str: return "%s[%s]" % (self.package_name, ' '.join(strs)) +@mypyc_attr(serializable=True) class _PackageVariantSlice(_Common): """A subset of a variant list, but with more dependency-related info.""" def __init__(self, package_name: str, entries: list[_PackageEntry], solver: Solver) -> None: @@ -611,11 +633,13 @@ def range_(self) -> VersionRange: @property def fam_requires(self) -> set[str]: self._update_fam_info() + assert self._fam_requires is not None return self._fam_requires @property - def common_fams(self): + def common_fams(self) -> set[str]: self._update_fam_info() + assert self._common_fams is not None return self._common_fams @property @@ -754,18 +778,19 @@ def extract(self) -> tuple[_PackageVariantSlice, Requirement | None]: fam = sorted(extractable)[0] last_range: VersionRange | None = None - ranges = set() + ranges_set = set() for variant in self.iter_variants(): req = variant.get(fam) + assert req is not None if req.range != last_range: # will match often, avoids set search - ranges.add(req.range) + ranges_set.add(req.range) last_range = req.range slice_ = copy.copy(self) slice_.extracted_fams = self.extracted_fams | set([fam]) - ranges = list(ranges) + ranges = list(ranges_set) range_ = ranges[0].union(ranges[1:]) common_req = Requirement.construct(fam, range_) return slice_, common_req @@ -836,6 +861,7 @@ def _split(i_entry: int, for j, variant in enumerate(entry.variants): fams = fams & variant.request_fams if not fams: + assert prev is not None return _split(*prev) prev = (i, j + 1, fams) @@ -968,6 +994,7 @@ def get_variant_slice(self, package_name: str, range_: VersionRange) -> _Package return slice_ +@mypyc_attr(serializable=True) class _PackageScope(_Common): """Contains possible solutions for a package, such as a list of variants, or a conflict range. As the resolve progresses, package scopes are narrowed @@ -1051,6 +1078,8 @@ def intersect(self, range_: VersionRange) -> _PackageScope | None: new_slice = self.solver._get_variant_slice( self.package_name, new_range) else: + assert self.variant_slice is not None, \ + "variant_slice should always exist for non-conflicted non-ephemeral requests" new_slice = self.variant_slice.intersect(range_) # intersection reduced the scope to nothing @@ -1087,6 +1116,8 @@ def reduce_by(self, package_request: Requirement) -> tuple[_PackageScope | None, return (self, []) # perform the reduction + assert self.variant_slice is not None, \ + "variant_slice should always exist for non-conflicted non-ephemeral requests" new_slice, reductions = self.variant_slice.reduce_by(package_request) # there was total reduction @@ -1127,6 +1158,9 @@ def extract(self) -> tuple[_PackageScope, Requirement | None]: if self.is_conflict or self.is_ephemeral: return (self, None) + assert self.variant_slice is not None, \ + "variant_slice should always exist for non-conflicted non-ephemeral requests" + new_slice, package_request = self.variant_slice.extract() if not package_request: return (self, None) @@ -1149,10 +1183,15 @@ def split(self) -> tuple[_PackageScope, _PackageScope] | None: if ( self.is_conflict or self.is_ephemeral - or len(self.variant_slice) == 1 ): return None + assert self.variant_slice is not None, \ + "variant_slice should always exist for non-conflicted non-ephemeral requests" + + if len(self.variant_slice) == 1: + return None + r = self.variant_slice.split() if r is None: return None @@ -1173,7 +1212,8 @@ def _is_solved(self) -> bool: self.is_conflict or self.is_ephemeral or ( - len(self.variant_slice) == 1 + self.variant_slice is not None # should never be None here + and len(self.variant_slice) == 1 and not self.variant_slice.extractable ) ) @@ -1235,6 +1275,7 @@ def _get_dependency_order(g: digraph, node_list: list[T]) -> list[T]: return ordered_nodes +@mypyc_attr(serializable=True) class _ResolvePhase(_Common): """A resolve phase contains a full copy of the resolve state, and runs the resolve algorithm until no further action can be taken without 'selecting' @@ -1318,15 +1359,15 @@ def _create_phase(status: SolverStatus | None = None) -> _ResolvePhase: # simplify extractions (there may be overlaps) self.pr.subheader("MERGE-EXTRACTIONS:") - extracted_requests = RequirementList(extracted_requests) + extracted_reqlist = RequirementList(extracted_requests) - if extracted_requests.conflict: # extractions are in conflict - req1, req2 = extracted_requests.conflict + if extracted_reqlist.conflict: # extractions are in conflict + req1, req2 = extracted_reqlist.conflict conflict = DependencyConflict(req1, req2) failure_reason = DependencyConflicts([conflict]) return _create_phase(SolverStatus.failed) elif self.pr: - self.pr("merged extractions: %s", extracted_requests) + self.pr("merged extractions: %s", extracted_reqlist) # intersect extracted requests with current scopes self.pr.subheader("INTERSECTING:") @@ -1334,27 +1375,27 @@ def _create_phase(status: SolverStatus | None = None) -> _ResolvePhase: with self.solver.timed(self.solver.intersection_test_time): for i, scope in enumerate(scopes): - extracted_req = extracted_requests.get(scope.package_name) + extracted_req = extracted_reqlist.get(scope.package_name) if extracted_req is None: continue # perform the intersection - scope_ = scope.intersect(extracted_req.range) + new_scope = scope.intersect(extracted_req.range) req_fams.append(extracted_req.name) - if scope_ is None: + if new_scope is None: # the scope conflicted with the extraction conflict = DependencyConflict( extracted_req, scope.package_request) failure_reason = DependencyConflicts([conflict]) return _create_phase(SolverStatus.failed) - if scope_ is not scope: + if new_scope is not scope: # the scope was narrowed because it intersected # with an extraction - scopes[i] = scope_ + scopes[i] = new_scope changed_scopes_i.add(i) self.solver.intersections_count += 1 @@ -1374,7 +1415,7 @@ def _create_phase(status: SolverStatus | None = None) -> _ResolvePhase: # add new scopes new_extracted_reqs = [ - x for x in extracted_requests.requirements + x for x in extracted_reqlist.requirements if x.name not in req_fams] if new_extracted_reqs: @@ -1461,10 +1502,10 @@ def _create_phase(status: SolverStatus | None = None) -> _ResolvePhase: # A different order here wouldn't cause an invalid solve, however # rez solves must be deterministic, so this is why we sort. # - pending_reducts = sorted(pending_reducts) + sorted_reducts = sorted(pending_reducts) - while pending_reducts: - x, y = pending_reducts.pop() + while sorted_reducts: + x, y = sorted_reducts.pop() if x == y: continue @@ -1481,7 +1522,7 @@ def _create_phase(status: SolverStatus | None = None) -> _ResolvePhase: # other scopes need to reduce against x again for j in all_scopes_i: if j != x: - pending_reducts.append((j, x)) + sorted_reducts.append((j, x)) changed_scopes_i = set() @@ -1498,6 +1539,8 @@ def finalise(self) -> _ResolvePhase: """ assert self._is_solved() g = self._get_minimal_graph() + assert g is not None, "graph should always be present when solved" + scopes = dict((x.package_name, x) for x in self.scopes if not x.is_conflict) @@ -1508,11 +1551,12 @@ def finalise(self) -> _ResolvePhase: for fam in fam_cycle: scope = scopes[fam] variant = scope._get_solved_variant() + assert variant is not None, "variant should not be None when scope is solved" stmt = VersionedObject.construct(fam, variant.version) cycle.append(stmt) phase = copy.copy(self) - phase.scopes = scopes.values() + phase.scopes = list(scopes.values()) phase.failure_reason = Cycle(cycle) phase.status = SolverStatus.cyclic return phase @@ -1751,6 +1795,7 @@ def _add_reduct_node(request: Requirement) -> str: reqlist = RequirementList(requests) if not reqlist.conflict: merged_request = reqlist.get(fam) + assert merged_request is not None for request in requests: if merged_request != request: id1 = _add_request_node(request) @@ -1971,12 +2016,12 @@ def __init__(self, self.optimised = optimised # these values are all set in _init() - self.phase_stack: list[_ResolvePhase] = None - self.failed_phase_list: list[_ResolvePhase] = None - self.depth_counts: dict = None - self.solve_begun: bool = None - self.solve_time: float = None - self.load_time: float = None + self.phase_stack: list[_ResolvePhase] + self.failed_phase_list: list[_ResolvePhase] + self.depth_counts: dict + self.solve_begun: bool + self.solve_time: float + self.load_time: float self.abort_reason: str | None = None self.callback_return: SolverCallbackReturn | None = None @@ -2293,6 +2338,7 @@ def get_graph(self) -> digraph: st = self.status if st in (SolverStatus.solved, SolverStatus.unsolved): phase = self._latest_nonfailed_phase() + assert phase is not None, "Should only be None if status is failed" return phase.get_graph() else: return self.get_fail_graph() @@ -2317,7 +2363,7 @@ def dump(self) -> None: for i, phase in enumerate(self.phase_stack): rows.append((self._depth_label(i), phase.status, str(phase))) - print("status: %s (%s)" % (self.status.name, self.status.description)) + print("status: %s (%s)" % (self.status.name, self.status.value[0])) print("initial request: %s" % str(self.request_list)) print() print("solve stack:") diff --git a/src/rez/status.py b/src/rez/status.py index 293044d6da..a5ed968628 100644 --- a/src/rez/status.py +++ b/src/rez/status.py @@ -10,7 +10,7 @@ from fnmatch import fnmatch from rez import __version__ from rez.utils.typing import SupportsWrite -from rez.utils.data_utils import cached_property +from functools import cached_property from rez.resolved_context import ResolvedContext from rez.packages import iter_packages, Package from rez.suite import Suite @@ -160,19 +160,19 @@ def print_tools(self, pattern: str | None = None, buf: SupportsWrite = sys.stdou if pattern and not fnmatch(tool, pattern): continue - labels = [] + label_parts = [] color = None path = which(tool) if path: path_ = os.path.join(suite.tools_path, tool) if path != path_: - labels.append("(hidden by unknown tool '%s')" % path) + label_parts.append("(hidden by unknown tool '%s')" % path) color = warning variant = d["variant"] if isinstance(variant, set): pkg_str = ", ".join(variant) - labels.append("(in conflict)") + label_parts.append("(in conflict)") color = critical else: pkg_str = variant.qualified_package_name @@ -181,11 +181,11 @@ def print_tools(self, pattern: str | None = None, buf: SupportsWrite = sys.stdou if orig_tool == tool: orig_tool = '-' - label = ' '.join(labels) + label = ' '.join(label_parts) source = ("context '%s' in suite '%s'" % (d["context_name"], suite.load_path)) - rows.append([tool, orig_tool, pkg_str, source, label, color]) + rows.append((tool, orig_tool, pkg_str, source, label, color)) seen.add(tool) _pr = Printer(buf) @@ -193,13 +193,13 @@ def print_tools(self, pattern: str | None = None, buf: SupportsWrite = sys.stdou _pr("No matching tools.") return False - headers = [["TOOL", "ALIASING", "PACKAGE", "SOURCE", "", None], - ["----", "--------", "-------", "------", "", None]] + headers = [("TOOL", "ALIASING", "PACKAGE", "SOURCE", "", None), + ("----", "--------", "-------", "------", "", None)] rows = headers + sorted(rows, key=lambda x: x[0].lower()) print_colored_columns(_pr, rows) return True - def _print_tool_info(self, value, buf=sys.stdout, b=False): + def _print_tool_info(self, value, buf=sys.stdout, b: bool = False) -> bool: word = "is also" if b else "is" _pr = Printer(buf) diff --git a/src/rez/suite.py b/src/rez/suite.py index d10d0e81d7..fe1b7fa89a 100644 --- a/src/rez/suite.py +++ b/src/rez/suite.py @@ -7,7 +7,7 @@ from rez.utils.execution import create_forwarding_script from rez.exceptions import SuiteError, ResolvedContextError from rez.resolved_context import ResolvedContext -from rez.utils.data_utils import cached_property +from functools import cached_property from rez.utils.filesystem import safe_rmtree from rez.utils.formatting import columnise, PackageRequest from rez.utils.colorize import warning, critical, Printer, alias as alias_col @@ -332,6 +332,7 @@ def get_tools(self): a tool of the same name), this will be a set of Variants. """ self._update_tools() + assert self.tools is not None return self.tools def get_tool_filepath(self, tool_alias): @@ -412,6 +413,7 @@ def get_alias_conflicts(self, tool_alias: str) -> list[Tool] | None: - variant (`Variant`): Variant providing the tool. """ self._update_tools() + assert self.tool_conflicts is not None return self.tool_conflicts.get(tool_alias) def validate(self) -> None: @@ -643,9 +645,9 @@ def _get_row(entry): else: context_names = sorted(self.contexts.keys()) - rows = [["TOOL", "ALIASING", "PACKAGE", "CONTEXT", ""], - ["----", "--------", "-------", "-------", ""]] - colors = [None, None] + rows = [("TOOL", "ALIASING", "PACKAGE", "CONTEXT", ""), + ("----", "--------", "-------", "-------", "")] + colors: list[Callable[[str], str] | None] = [None, None] entries_dict = defaultdict(list) for d in self.get_tools().values(): diff --git a/src/rez/system.py b/src/rez/system.py index 310f4b803a..b4e0f04468 100644 --- a/src/rez/system.py +++ b/src/rez/system.py @@ -12,7 +12,7 @@ from rez import __version__ from rez.utils.platform_ import platform_ from rez.exceptions import RezSystemError -from rez.utils.data_utils import cached_property +from functools import cached_property class System(object): diff --git a/src/rez/tests/test_package_repository.py b/src/rez/tests/test_package_repository.py index 7d058d28b8..70547acfc4 100644 --- a/src/rez/tests/test_package_repository.py +++ b/src/rez/tests/test_package_repository.py @@ -37,9 +37,9 @@ def test_mismatching_case(self): case_mismatch_package = create_package("MyTestPackage", data={}) case_mismatch_variant = next(case_mismatch_package.iter_variants()) - pkg_repository._create_variant(variant, overrides={}) + pkg_repository._create_variant(variant.resource, overrides={}) with self.assertRaises(filesystem.PackageRepositoryError): - pkg_repository._create_variant(case_mismatch_variant, overrides={}) + pkg_repository._create_variant(case_mismatch_variant.resource, overrides={}) @unittest.skipIf( @@ -74,7 +74,7 @@ def _install(self, repo, name, version_str): data = {"version": version_str} if version_str else {} package = create_package(name, data=data) variant = next(package.iter_variants()) - return repo._create_variant(variant, overrides={}) + return repo._create_variant(variant.resource, overrides={}) def test_get_package_from_uri_uppercase_version(self): """get_package_from_uri finds a package whose version contains uppercase letters.""" diff --git a/src/rez/tests/test_packages_order.py b/src/rez/tests/test_packages_order.py index 12c0ebe7c3..abff2d2bd1 100644 --- a/src/rez/tests/test_packages_order.py +++ b/src/rez/tests/test_packages_order.py @@ -9,7 +9,8 @@ from rez.config import config from rez.package_order import NullPackageOrder, PackageOrder, PerFamilyOrder, VersionSplitPackageOrder, \ - TimestampPackageOrder, SortedOrder, PackageOrderList, from_pod + TimestampPackageOrder, SortedOrder, from_pod +from rez.package_order_list import PackageOrderList from rez.packages import iter_packages from rez.tests.util import TestBase, TempdirMixin from rez.version import Version diff --git a/src/rez/tests/test_solver.py b/src/rez/tests/test_solver.py index f076067ebf..20284ebb05 100644 --- a/src/rez/tests/test_solver.py +++ b/src/rez/tests/test_solver.py @@ -39,7 +39,7 @@ def _create_solvers(self, reqs): s_perms = [] perms = itertools.permutations(reqs) for reqs_ in perms: - s = Solver(reqs_, + s = Solver(list(reqs_), self.packages_path, optimised=True, verbosity=solver_verbosity) diff --git a/src/rez/util.py b/src/rez/util.py index 91e7a6b745..74427b66f0 100644 --- a/src/rez/util.py +++ b/src/rez/util.py @@ -8,8 +8,8 @@ """ from __future__ import annotations -import collections.abc import atexit +import collections.abc import os import os.path import re diff --git a/src/rez/utils/_mypyc.py b/src/rez/utils/_mypyc.py new file mode 100644 index 0000000000..2509718861 --- /dev/null +++ b/src/rez/utils/_mypyc.py @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright Contributors to the Rez Project + + +"""Resilient access to ``mypy_extensions.mypyc_attr``. + +The ``@mypyc_attr`` decorator configures how mypyc compiles a class (eg +``allow_interpreted_subclasses``). It is required when building rez with mypyc +and is a no-op at runtime. ``mypy_extensions`` is a declared runtime +requirement (see ``setup.py``), but it is not importable in every context -- +notably ``install.py`` imports rez modules using the system python, before +rez's dependencies have been installed. Import ``mypyc_attr`` from here rather +than directly from ``mypy_extensions`` so that importing rez does not +hard-require ``mypy_extensions``; when it is absent we fall back to a no-op +decorator (which is all it does at runtime anyway). +""" + +try: + from mypy_extensions import mypyc_attr +except ImportError: + def mypyc_attr(*attrs, **kwattrs): # type: ignore[misc] + def deco(x): + return x + return deco diff --git a/src/rez/utils/data_utils.py b/src/rez/utils/data_utils.py index bc20f0b650..84c2d5b665 100644 --- a/src/rez/utils/data_utils.py +++ b/src/rez/utils/data_utils.py @@ -10,10 +10,16 @@ import os.path import json import functools +from functools import cached_property +import re -from rez.vendor.schema.schema import Schema, Optional +from rez.vendor.schema.schema import Schema, Optional, Or, And from threading import Lock -from typing import Any, Callable, Generic, MutableMapping, TypeVar, TYPE_CHECKING +from typing import Any, Callable, Generic, Mapping, MutableMapping, TypeVar, TYPE_CHECKING, NoReturn + +if TYPE_CHECKING: + import rez.config + from rez.utils.resources import ResourceWrapper T = TypeVar("T") @@ -24,6 +30,7 @@ class ModifyList(object): This can be used in configs to add to list-based settings, rather than overwriting them. """ + def __init__(self, append=None, prepend=None) -> None: for v in (prepend, append): if v is not None and not isinstance(v, list): @@ -49,7 +56,8 @@ class DelayLoad(object): - yaml (``*.yaml``, ``*.yml``) - json (``*.json``) """ - def __init__(self, filepath) -> None: + + def __init__(self, filepath: str) -> None: self.filepath = os.path.expanduser(filepath) def __str__(self) -> str: @@ -93,13 +101,13 @@ def _json(contents): ) -def remove_nones(**kwargs): +def remove_nones(**kwargs: Any | None) -> dict[str, Any]: """Return diict copy with nones removed. """ return dict((k, v) for k, v in kwargs.items() if v is not None) -def deep_update(dict1, dict2) -> None: +def deep_update(dict1: dict, dict2: dict) -> None: """Perform a deep merge of `dict2` into `dict1`. Note that `dict2` and any nested dicts are unchanged. @@ -137,7 +145,7 @@ def merge(v1, v2): dict1[k2] = merge(v1, v2) -def deep_del(data, fn): +def deep_del(data: dict, fn) -> dict: """Create dict copy with removed items. Recursively remove items where fn(value) is True. @@ -157,7 +165,7 @@ def deep_del(data, fn): return result -def get_dict_diff(d1, d2): +def get_dict_diff(d1: dict, d2: dict) -> tuple[list, list, list]: """Get added/removed/changed keys between two dicts. Each key in the return value is a list, which is the namespaced key that @@ -198,7 +206,7 @@ def _diff(d1_, d2_, namespace): return _diff(d1, d2, []) -def get_dict_diff_str(d1, d2, title): +def get_dict_diff_str(d1: dict, d2: dict, title: str) -> str: """Returns same as `get_dict_diff`, but as a readable string. """ added, removed, changed = get_dict_diff(d1, d2) @@ -217,55 +225,6 @@ def get_dict_diff_str(d1, d2, title): return '\n'.join(lines) -if TYPE_CHECKING: - cached_property = property -else: - class cached_property(object): - """Simple property caching descriptor. - - Example: - - >>> class Foo(object): - >>> @cached_property - >>> def bah(self): - >>> print('bah') - >>> return 1 - >>> - >>> f = Foo() - >>> f.bah - bah - 1 - >>> f.bah - 1 - """ - def __init__(self, func, name=None) -> None: - self.func = func - # Make sure that Sphinx autodoc can follow and get the docstring from our wrapped function. - functools.update_wrapper(self, func) - self.name = name or func.__name__ - - def __get__(self, instance, owner=None): - if instance is None: - return self - - result = self.func(instance) - try: - setattr(instance, self.name, result) - except AttributeError: - raise AttributeError("can't set attribute %r on %r" - % (self.name, instance)) - return result - - # This is to silence Sphinx that complains that cached_property is not a callable. - def __call__(self): - raise RuntimeError("@cached_property should not be called.") - - @classmethod - def uncache(cls, instance, name) -> None: - if hasattr(instance, name): - delattr(instance, name) - - class cached_class_property(Generic[T]): """Simple class property caching descriptor. @@ -283,16 +242,17 @@ class cached_class_property(Generic[T]): >>> Foo.bah 1 """ + def __init__(self, func: Callable[[Any], T], name=None) -> None: self.func = func # Make sure that Sphinx autodoc can follow and get the docstring from our wrapped function. # TODO: Doesn't work... functools.update_wrapper(self, func) # type: ignore[arg-type] - def __get__(self, instance, owner=None) -> T: + def __get__(self, instance: object, owner: type | None = None) -> T: assert owner name = "_class_property_" + self.func.__name__ - result = getattr(owner, name, KeyError) + result: T | type[KeyError] = getattr(owner, name, KeyError) if result is KeyError: result = self.func(owner) @@ -302,7 +262,8 @@ def __get__(self, instance, owner=None) -> T: class LazySingleton(Generic[T]): """A threadsafe singleton that initialises when first referenced.""" - def __init__(self, instance_class: type[T], *nargs, **kwargs) -> None: + + def __init__(self, instance_class: type[T], *nargs: Any, **kwargs: Any) -> None: self.instance_class = instance_class self.nargs = nargs self.kwargs = kwargs @@ -334,6 +295,7 @@ class AttrDictWrapper(MutableMapping[str, Any]): >>> assert dd.one == 1 >>> assert d['one'] == 1 """ + def __init__(self, data=None) -> None: self.__dict__['_data'] = {} if data is None else data @@ -352,22 +314,22 @@ def __getattr__(self, attr: str) -> Any: raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, attr)) - def __setattr__(self, attr, value) -> None: + def __setattr__(self, attr: str, value) -> None: # For things like '__class__', for instance if attr.startswith('__') and attr.endswith('__'): super(AttrDictWrapper, self).__setattr__(attr, value) self._data[attr] = value - def __getitem__(self, key): + def __getitem__(self, key: str): return self._data[key] - def __setitem__(self, key, value) -> None: + def __setitem__(self, key: str, value) -> None: self._data[key] = value - def __delitem__(self, key) -> None: + def __delitem__(self, key: str) -> None: del self._data[key] - def __contains__(self, key) -> bool: + def __contains__(self, key: str) -> bool: return key in self._data def __iter__(self): @@ -388,13 +350,15 @@ def copy(self): class RO_AttrDictWrapper(AttrDictWrapper): """Read-only version of AttrDictWrapper.""" - def __setattr__(self, attr, value) -> None: + + def __setattr__(self, attr: str, value: object) -> NoReturn: self[attr] # may raise 'no attribute' error raise AttributeError("'%s' object attribute '%s' is read-only" % (self.__class__.__name__, attr)) -def convert_dicts(d, to_class=AttrDictWrapper, from_class=dict): +def convert_dicts(d: Mapping, to_class: type[MutableMapping] = AttrDictWrapper, + from_class: type[MutableMapping] = dict) -> MutableMapping: """Recursively convert dict and UserDict types. Note that `d` is unchanged. @@ -418,7 +382,9 @@ def convert_dicts(d, to_class=AttrDictWrapper, from_class=dict): return d_ -def get_object_completions(instance, prefix, types=None, instance_types=None): +def get_object_completions(instance: object, prefix: str, + types: tuple[type, ...] | None = None, + instance_types: tuple[type, ...] | None = None) -> list[str]: """Get completion strings based on an object's attributes/keys. Completion also works on dynamic attributes (eg implemented via __getattr__) @@ -480,7 +446,7 @@ def get_object_completions(instance, prefix, types=None, instance_types=None): return qual_words -def convert_json_safe(value): +def convert_json_safe(value: Any) -> Any: """Convert data to JSON safe values. Anything not representable (eg python objects) will be stringified. @@ -544,21 +510,30 @@ class AttributeForwardMeta(type): >>> print(y.c) None """ - def __new__(cls, name, parents, members): + def __new__(cls, name: str, parents: tuple[type, ...], members: dict[str, Any]) -> AttributeForwardMeta: + attr_info = cls._get_new_attrs(members, parents) + + for key in sorted(attr_info): + members[key] = cls._make_forwarder(key) + + return super(AttributeForwardMeta, cls).__new__(cls, name, parents, members) + + @classmethod + def _get_new_attrs(cls, members, parents): def _defined(x): return x in members or any(hasattr(p, x) for p in parents) + attr_info = {} keys = members.get('keys') if keys: for key in keys: if not _defined(key): - members[key] = cls._make_forwarder(key) - - return super(AttributeForwardMeta, cls).__new__(cls, name, parents, members) + attr_info[key] = _defined("_wrap_forwarded") + return attr_info @classmethod - def _make_forwarder(cls, key): - def func(self): + def _make_forwarder(cls, key: str) -> property: + def func(self: ResourceWrapper) -> Any: value = getattr(self.wrapped, key, None) if hasattr(self, "_wrap_forwarded"): @@ -568,6 +543,24 @@ def func(self): return property(func) + @classmethod + def get_getters(cls, typ) -> dict[str, dict]: + result = {} + attr_info = cls._get_new_attrs(typ.__dict__, typ.mro()) + + for attr in sorted(attr_info): + has_wrap_forwarded = attr_info[attr] + s = " @property\n" + s += " def {key}(self) -> {typestr}:\n" + + if has_wrap_forwarded: + s += " return self._wrap_forwarded('{key}', self.wrapped.{key})\n" + else: + s += " return self.wrapped.{key}\n" + s += "\n" + result[attr] = {"key": attr, "template": s} + return result + class LazyAttributeMeta(type): """Metaclass for adding properties to a class for accessing top-level keys @@ -594,13 +587,27 @@ class LazyAttributeMeta(type): your own '_validate_key' function; - '_schema_keys' (frozenset): Keys in the schema. """ - def __new__(cls, name, parents, members): - def _defined(x): + def __new__(cls, name: str, parents: tuple[type, ...], members: dict[str, Any]) -> LazyAttributeMeta: + attr_info, add_extras = cls._get_new_attrs(members, parents) + if attr_info: + for attr, info in attr_info.items(): + members[attr] = cls._make_getter(info["key"], info["attr"], + info["optional"], info["key_schema"]) + if add_extras: + members["validate_data"] = cls._make_validate_data() + members["validated_data"] = cls._make_validated_data() + members["_validate_key_impl"] = cls._make_validate_key_impl() + members["_schema_keys"] = frozenset(attr_info) + + return super(LazyAttributeMeta, cls).__new__(cls, name, parents, members) + + @classmethod + def _get_new_attrs(cls, members, parents): + def _defined(x: str) -> bool: return x in members or any(hasattr(p, x) for p in parents) schema = members.get('schema') - keys = set() - + attr_info = {} if schema: schema_dict = schema._schema for key, key_schema in schema_dict.items(): @@ -608,8 +615,6 @@ def _defined(x): while isinstance(key, Schema): key = key._schema if isinstance(key, str): - keys.add(key) - if _defined(key): attr = "_%s" % key if _defined(attr): @@ -617,26 +622,21 @@ def _defined(x): "%r, already defined" % attr) else: attr = key + attr_info[attr] = { + "key": key, "attr": attr, "optional": optional, "key_schema": key_schema, + } - members[attr] = cls._make_getter(key, attr, optional, key_schema) - - if schema or not _defined("schema"): - members["validate_data"] = cls._make_validate_data() - members["validated_data"] = cls._make_validated_data() - members["_validate_key_impl"] = cls._make_validate_key_impl() - members["_schema_keys"] = frozenset(keys) - - return super(LazyAttributeMeta, cls).__new__(cls, name, parents, members) + return attr_info, schema or not _defined("schema") @classmethod - def _make_validate_data(cls): + def _make_validate_data(cls) -> Callable[[Any], None]: def func(self) -> None: self.validated_data() return func @classmethod - def _make_validated_data(cls): - def func(self): + def _make_validated_data(cls) -> Callable[[Any], dict[str, Any] | None]: + def func(self: Any) -> dict[str, Any] | None: if self.schema: d = {} for key in self._schema_keys: @@ -655,8 +655,8 @@ def func(self): return func @classmethod - def _make_validate_key_impl(cls): - def func(self, key, attr, schema): + def _make_validate_key_impl(cls) -> Callable[[Any, str, str, Schema], dict[str, Any]]: + def func(self: Any, key: str, attr: str, schema: Schema) -> dict[str, Any]: schema_ = schema if isinstance(schema, Schema) else Schema(schema) try: return schema_.validate(attr) @@ -666,7 +666,8 @@ def func(self, key, attr, schema): return func @classmethod - def _make_getter(cls, key, attribute, optional, key_schema): + def _make_getter(cls, key: str, attribute: str, optional: bool, key_schema: rez.config.Setting) -> cached_property: + def getter(self): if key not in (self._data or {}): if optional: @@ -679,4 +680,348 @@ def getter(self): else: return self._validate_key_impl(key, attr, key_schema) - return cached_property(getter, name=attribute) + prop = cached_property(getter) + # prop._getter_info = {"key": key, "optional": optional, "schema": key_schema} + return prop + + @classmethod + def get_getters(cls, typ, extra_members: dict | None = None): + result = {} + members = extra_members.copy() or {} + members.update(typ.__dict__) + attr_info, make_extras = cls._get_new_attrs(members, typ.mro()) + for name in sorted(attr_info): + info = attr_info[name] + key = info["key"] + + typestr = get_typing_typestr(info["key_schema"]) + + optional = info["optional"] + if optional: + typestr = f"{typestr} | None" + + s = " @cached_property\n" + s += " def {name}(self) -> {typestr}:\n" + s += f" return self._get_item({repr(key)}, {optional})\n" + s += "\n" + result[key] = {"key": key, "name": name, "typestr": typestr, "template": s} + return result + + +BEGIN = " # -- BEGIN AUTO-GENERATED METHODS --\n" +END = " # -- END AUTO-GENERATED METHODS --\n" + + +# Return-type overrides for generated forwarder accessors, keyed by +# (class name, attribute name). Some forwarded attributes have a narrower +# runtime type than the wrapped object's schema implies. For example a package +# always exposes a Version, but 'version' is Optional in the pod schema and so +# would otherwise be generated as 'Version | None'. Declaring the override here +# keeps the generated source correct without hand-editing it after generation. +FORWARDER_TYPE_OVERRIDES = { + ("Package", "version"): "Version", +} + + +def write_module_dynamic_members(module, wrapped=None): + import inspect + + wrapped = wrapped if wrapped else {} + + for name, obj in inspect.getmembers(module): + if isinstance(obj, type) and obj.__module__ == module.__name__: + write_cls_dynamic_members(obj, wrapped_cls=wrapped.get(name)) + + +def remove_dynamic_members(module_name: str): + import importlib.util + + spec = importlib.util.find_spec(module_name) + assert spec and spec.origin + + skip = False + lines = [] + with open(spec.origin) as f: + for line in f: + if line == END: + skip = False + + if skip: + continue + else: + lines.append(line) + + if line == BEGIN: + skip = True + with open(spec.origin, "w") as f: + f.writelines(lines) + + +def write_cls_dynamic_members(obj, wrapped_cls=None, source_cls=None): + name = obj.__name__ + + print(name) + + if source_cls is None: + source_cls = obj + + forward_getters = AttributeForwardMeta.get_getters(source_cls) + lazy_getters = LazyAttributeMeta.get_getters(source_cls, forward_getters) + + if forward_getters or lazy_getters: + # FIXME: I think we only need to do this if forward_getters exists + if wrapped_cls and wrapped_cls.schema is not None: + schema = wrapped_cls.schema._schema + key_schemas = {} + for key, value in schema.items(): + if isinstance(key, Optional): + optional = True + key = key._schema + else: + optional = False + key_schemas[key] = (value, optional) + else: + key_schemas = None + optional = None + + members = [] + for key in sorted(forward_getters): + data = forward_getters[key] + + typestr = "typing.Any" + if key_schemas is not None: + info = key_schemas.get(key) + if info: + key_schema, optional = info + typestr = get_typing_typestr(key_schema) + if optional: + typestr = f"{typestr} | None" + + override = FORWARDER_TYPE_OVERRIDES.get((obj.__name__, key)) + if override is not None: + typestr = override + + data["typestr"] = typestr + members.append(data["template"].format(**data)) + + for key in sorted(lazy_getters): + data = lazy_getters[key] + members.append(data["template"].format(**data)) + + assert members + + import sys + + module_path = sys.modules[obj.__module__].__file__ + print(f" Adding {len(members)} members to {module_path}") + + with open(module_path, "r") as f: + all_lines = f.readlines() + + in_class = False + reg = re.compile(rf"class {obj.__name__}\b") + for i, line in enumerate(all_lines): + if in_class and line == BEGIN: + print(" Adding lines") + all_lines[i + 1: i + 1] = members + break + if reg.match(line): + in_class = True + else: + raise RuntimeError("Could not find BEGIN") + + with open(module_path, "w") as f: + f.writelines(all_lines) + + +def write_all_dynamic_members(): + """ + ResourceWrapper (metaclass=AttributeForwardMeta) + PackageRepositoryResourceWrapper + PackageBaseResourceWrapper + Package [keys] + DeveloperPackage + Variant [keys] + PackageFamily [keys] + Resource (metaclass=LazyAttributeMeta) + PackageRepositoryResource + PackageFamilyResource + MemoryPackageFamilyResource + FileSystemPackageFamilyResource + FileSystemCombinedPackageFamilyResource [schema] + PackageResource + VariantResource + VariantResourceHelper [keys] [schema] (metaclass=(AttributeForwardMeta, LazyAttributeMeta)) + MemoryVariantResource + FileSystemVariantResource + FileSystemCombinedVariantResource + PackageResourceHelper + MemoryPackageResource [schema] + FileSystemPackageResource [schema] + FileSystemCombinedPackageResource [schema] + + VariantResourceHelper is the first in its hierarchy to introduce self.wrapped and therefore the + AttributeForwardMeta, which adds the wrapped lookups + """ + + remove_dynamic_members("rez.packages") + remove_dynamic_members("rez.package_resources") + remove_dynamic_members("rezplugins.package_repository.filesystem") + + import rez.packages + import rez.package_resources + import rezplugins.package_repository.memory + import rezplugins.package_repository.filesystem + + write_cls_dynamic_members(rez.packages.Package, + wrapped_cls=rezplugins.package_repository.memory.MemoryPackageResource) + + write_cls_dynamic_members(rez.packages.Variant, + wrapped_cls=rez.package_resources.VariantResourceHelper) + + write_cls_dynamic_members(rez.packages.PackageFamily, + wrapped_cls=rezplugins.package_repository.memory.MemoryPackageFamilyResource) + + # we graft this from MemoryPackageResource because all the children have the same schema, and + # putting the members on the base class makes mypy happy + write_cls_dynamic_members(rez.package_resources.PackageResourceHelper, + source_cls=rezplugins.package_repository.memory.MemoryPackageResource) + + write_cls_dynamic_members(rez.package_resources.VariantResourceHelper, + wrapped_cls=rezplugins.package_repository.memory.MemoryPackageResource) + + write_cls_dynamic_members(rezplugins.package_repository.filesystem.FileSystemCombinedPackageFamilyResource) + + return + + # FIXME: based on the hierarchy above, we should be able to run this on the + # five specific classes that introduce keys or schema attributes. + import rez.utils.resources + import rez.package_repository + import rez.package_resources + import rez.packages + import rezplugins.package_repository.filesystem + import rezplugins.package_repository.memory + import rez.config + + write_module_dynamic_members(rez.utils.resources) + write_module_dynamic_members(rez.package_repository) + write_module_dynamic_members( + rez.package_resources, + # VariantResourceHelper used both AttributeForwardMeta & LazyAttributeMeta. + # It wraps PackageResourceHelper, but that's abstract so we get the wrapped schema from one of the + # three concrete subclasses of PackageResourceHelper + wrapped={"VariantResourceHelper": rezplugins.package_repository.memory.MemoryPackageResource} + ) + write_module_dynamic_members( + rez.packages, + # same story as VariantResourceHelper. yes, they both wrap the same object. IDK WTF... + wrapped={ + "Package": rezplugins.package_repository.memory.MemoryPackageResource, + "Variant": rez.package_resources.VariantResourceHelper, + "PackageFamily": rezplugins.package_repository.memory.MemoryPackageFamilyResource, + } + ) + + write_module_dynamic_members(rezplugins.package_repository.filesystem) + write_module_dynamic_members(rezplugins.package_repository.memory) + write_module_dynamic_members(rez.config) + + +def get_typing_typestr(obj, parent_setting=None): + import rez.config + + # FIXME: remove parent_setting. it was a failed TypedDict experiment + if isinstance(obj, Optional): + return "typing.Optional[{}]".format(get_typing_typestr(obj._schema, parent_setting=parent_setting)) + if isinstance(obj, Schema): + # FIXME: this is skipping over Optional, which inherits from Schema + return get_typing_typestr(obj._schema, parent_setting=parent_setting) + elif isinstance(obj, list): + return "list[{}]".format(get_typing_typestr(obj[0], parent_setting=parent_setting)) + elif isinstance(obj, dict): + items = [] + typeddict = True + all_items = list(obj.items()) + for key, value in all_items: + optional = False + if isinstance(key, Optional): + key = key._schema + + if value is object: + # an Optional key where the value is `object` indicates a dict that supports + # arbitrary keys. see extensible_schema_dict + pass + # we don't write this into the TypedDict + continue + else: + # a single optional key + optional = True + + if len(all_items) == 1 and not isinstance(key, str): + # case: dict[X, Y] + typeddict = False + + key_str = get_typing_typestr(key, parent_setting=parent_setting) + value_str = get_typing_typestr(value, parent_setting=parent_setting) + items.append((key_str, value_str, optional)) + + if typeddict: + # FIXME: not supported. Inlined typedDicts are not fully supported by mypy or other checkers. + # This means we'll have to generate TypedDict declarations which will be a PITA + return "dict[str, typing.Any]" + # inlined TypedDicts are not a standard feature yet. revisit + # assert parent_setting is not None + # if total: + # item_strs = [] + # for key_str, value_str, optional in items: + # if optional: + # key_str = f"typing.NotRequired[{key_str}]" + # item_strs.append(f"{key_str}: {value_str}") + # typestr = "{" + ", ".join(item_strs) + "}" + # return f"typing.TypedDict({repr(parent_setting)}, {typestr})" + # else: + # item_strs = [] + # for key_str, value_str, optional in items: + # if not optional: + # key_str = f"typing.Required[{key_str}]" + # item_strs.append(f"{key_str}: {value_str}") + # typestr = "{" + ", ".join(item_strs) + "}" + # return f"typing.TypedDict({repr(parent_setting)}, {typestr}, total=False)" + else: + assert len(items) == 1 + key_str, value_str, optional = items[0] + return f"dict[{key_str}, {value_str}]" + + elif isinstance(obj, type): + if issubclass(obj, rez.config.Setting): + return get_typing_typestr(obj.schema, parent_setting=parent_setting) + # FIXME: use names to avoid circular import (rez.config imports data_utils) + # if "Setting" in [t.__name__ for t in obj.mro()]: + # return get_typing_typestr(obj.schema, parent_setting=obj.__name__) + else: + return obj.__name__ + elif isinstance(obj, Or): + return "typing.Union[{}]".format( + ", ".join(get_typing_typestr(x, parent_setting=parent_setting) for x in obj._args)) + elif obj is callable: + return "typing.Callable" + elif obj is None: + return "None" + elif isinstance(obj, And): + if len(obj._args) == 2 and isinstance(obj._args[1], rez.config.Use): + # used to convert a type: e.g. And(str, Use(VersionRange)) + use = obj._args[1] + if isinstance(use._callable, type): + newtype = use._callable + elif isinstance(use, type): + newtype = use + else: + newtype = obj._args[0] + return get_typing_typestr(newtype, parent_setting=parent_setting) + return repr(obj) + elif isinstance(obj, str): + return repr(obj) + else: + # FIXME + return "typing.Any" diff --git a/src/rez/utils/filesystem.py b/src/rez/utils/filesystem.py index dd818b325d..22cea62126 100644 --- a/src/rez/utils/filesystem.py +++ b/src/rez/utils/filesystem.py @@ -117,6 +117,7 @@ def make_path_writable(path): yield finally: if new_mode != orig_mode: + assert orig_mode is not None os.chmod(path, orig_mode) diff --git a/src/rez/utils/formatting.py b/src/rez/utils/formatting.py index d0ac49aab0..b63dafdc18 100644 --- a/src/rez/utils/formatting.py +++ b/src/rez/utils/formatting.py @@ -12,12 +12,13 @@ from rez.exceptions import PackageRequestError from pprint import pformat from enum import Enum -from typing import Any, Sequence, Mapping, TYPE_CHECKING +from typing import Any, Sequence, Mapping, TYPE_CHECKING, ClassVar import math import os import os.path import re import time +from rez.utils._mypyc import mypyc_attr if TYPE_CHECKING: from rez.rex import RexExecutor @@ -166,14 +167,15 @@ def get_value(self, key: int | str, args: Sequence[Any], kwds: Mapping[str, Any] return Formatter.get_value(self, key, args, kwds) +@mypyc_attr(allow_interpreted_subclasses=True) class StringFormatMixin(object): """Turn any object into a string formatter. An object inheriting this mixin will have a `format` function added, that is able to format using attributes of the object. """ - format_expand = StringFormatType.error - format_pretty = True + format_expand: ClassVar[StringFormatType] = StringFormatType.error + format_pretty: ClassVar[bool] = True def format(self, s: str, pretty: bool | None = None, expand: StringFormatType | None = None) -> str: diff --git a/src/rez/utils/memcached.py b/src/rez/utils/memcached.py index 00d46f20c4..94a20608e0 100644 --- a/src/rez/utils/memcached.py +++ b/src/rez/utils/memcached.py @@ -11,13 +11,20 @@ from threading import local from contextlib import contextmanager from functools import update_wrapper -from inspect import isgeneratorfunction from hashlib import md5 from uuid import uuid4 from typing import Any, Callable, Iterator, TypeVar +# FIXME: remove this workaround once support for python 3.9 is dropped +try: + from typing import Concatenate, ParamSpec +except ImportError: + from rez.vendor.typing_extensions.typing_extensions import Concatenate, ParamSpec + CallableT = TypeVar("CallableT", bound=Callable) +T = TypeVar("T") +P = ParamSpec("P") # this version should be changed if and when the caching interface changes cache_interface_version = 2 @@ -202,14 +209,14 @@ class _ScopedInstanceManager(local): def __init__(self) -> None: self.clients: dict[tuple[tuple, bool], list] = {} - def acquire(self, servers, debug: bool = False) -> tuple[Client, tuple[tuple, bool]]: + def acquire(self, servers: list[str] | None, debug: bool = False) -> tuple[Client, tuple[tuple, bool]]: key = (tuple(servers or []), debug) entry = self.clients.get(key) if entry: entry[1] += 1 return entry[0], key else: - client = Client(servers, debug=debug) + client = Client(servers or [], debug=debug) self.clients[key] = [client, 1] return client, key @@ -228,7 +235,7 @@ def release(self, key: tuple[tuple, bool]) -> None: @contextmanager -def memcached_client(servers=config.memcached_uri, debug=config.debug_memcache) -> Iterator[Client]: +def memcached_client(servers: list[str] | None = config.memcached_uri, debug: bool = config.debug_memcache) -> Iterator[Client]: # noqa: E501 """Get a shared memcached instance. This function shares the same memcached instance across nested invocations. @@ -250,27 +257,30 @@ def memcached_client(servers=config.memcached_uri, debug=config.debug_memcache) scoped_instance_manager.release(key) -def pool_memcached_connections(func): +def pool_memcached_connections(func: Callable[P, T]) -> Callable[P, T]: """Function decorator to pool memcached connections. Use this to wrap functions that might make multiple calls to memcached. This will cause a single memcached client to be shared for all connections. """ - if isgeneratorfunction(func): - def wrapper(*nargs, **kwargs): - with memcached_client(): - for result in func(*nargs, **kwargs): - yield result - else: - def wrapper(*nargs, **kwargs): - with memcached_client(): - return func(*nargs, **kwargs) + # if isgeneratorfunction(func): + # def wrapper(*nargs: P.args, **kwargs: P.kwargs): + # with memcached_client(): + # for result in func(*nargs, **kwargs): + # yield result + # else: + def wrapper(*nargs: P.args, **kwargs: P.kwargs) -> T: + with memcached_client(): + return func(*nargs, **kwargs) return update_wrapper(wrapper, func) -def memcached(servers, key=None, from_cache=None, to_cache=None, time: int = 0, - min_compress_len: int = 0, debug: bool = False) -> Callable[[CallableT], CallableT]: +def memcached(servers: list[str] | None, key: Callable[P, str] | None = None, + from_cache: Callable[Concatenate[T, P], T] | None = None, + to_cache: Callable[Concatenate[T, P], T] | None = None, + time: int = 0, + min_compress_len: int = 0, debug: bool = False) -> Callable[[Callable[P, T]], Callable[P, T]]: """memcached memoization function decorator. The wrapped function is expected to return a value that is stored to a @@ -321,7 +331,7 @@ def _listdir(path): However this increases chances of key clashes so should not be left turned on. """ - def default_key(func, *nargs, **kwargs): + def default_key(func: Callable, *nargs: Any, **kwargs: Any) -> str: parts = [func.__module__] argnames = get_function_arg_names(func) @@ -348,15 +358,15 @@ def default_key(func, *nargs, **kwargs): return repr(value) - def identity(value, *nargs, **kwargs): + def identity(value: T, *nargs: P.args, **kwargs: P.kwargs) -> T: return value from_cache = from_cache or identity to_cache = to_cache or identity - def decorator(func): + def decorator(func: Callable[P, T]): if servers: - def wrapper(*nargs, **kwargs): + def wrapper(*nargs: P.args, **kwargs: P.kwargs) -> T: with memcached_client(servers, debug=debug) as client: if key: cache_key = key(*nargs, **kwargs) @@ -366,6 +376,7 @@ def wrapper(*nargs, **kwargs): # get result = client.get(cache_key) if result is not client.miss: + assert not isinstance(result, Client._Miss) return from_cache(result, *nargs, **kwargs) # cache miss - run target function @@ -381,7 +392,7 @@ def wrapper(*nargs, **kwargs): min_compress_len=min_compress_len) return result else: - def wrapper(*nargs, **kwargs): + def wrapper(*nargs: P.args, **kwargs: P.kwargs) -> T: result = func(*nargs, **kwargs) if isinstance(result, DoNotCache): return result.result @@ -398,8 +409,8 @@ def forget() -> None: with memcached_client(servers, debug=debug) as client: client.flush() - wrapper.forget = forget - wrapper.__wrapped__ = func + wrapper.forget = forget # type: ignore[attr-defined] + wrapper.__wrapped__ = func # type: ignore[attr-defined] return update_wrapper(wrapper, func) return decorator diff --git a/src/rez/utils/patching.py b/src/rez/utils/patching.py index c0d72b52a2..79037ae684 100644 --- a/src/rez/utils/patching.py +++ b/src/rez/utils/patching.py @@ -44,8 +44,8 @@ def get_patched_request(requires, patchlist): '^': (True, True, True) } - requires = [Requirement(x) if not isinstance(x, Requirement) else x - for x in requires] + requires: list[Requirement | None] = [ + Requirement(x) if not isinstance(x, Requirement) else x for x in requires] appended = [] for patch in patchlist: diff --git a/src/rez/utils/platform_.py b/src/rez/utils/platform_.py index b25616e338..9017718a83 100644 --- a/src/rez/utils/platform_.py +++ b/src/rez/utils/platform_.py @@ -11,7 +11,7 @@ import subprocess from rez.util import which from rez.utils.execution import Popen -from rez.utils.data_utils import cached_property +from functools import cached_property from rez.utils.platform_mapped import platform_mapped from rez.exceptions import RezSystemError from tempfile import gettempdir diff --git a/src/rez/utils/resources.py b/src/rez/utils/resources.py index fdf610f1e8..2cc118c73f 100644 --- a/src/rez/utils/resources.py +++ b/src/rez/utils/resources.py @@ -35,25 +35,57 @@ """ from __future__ import annotations -from functools import lru_cache +import importlib.machinery +import sys +from functools import lru_cache, cached_property -from rez.utils.data_utils import cached_property, AttributeForwardMeta, \ - LazyAttributeMeta -from rez.config import config -from rez.exceptions import ResourceError +# from functools import cached_property +from rez.exceptions import ResourceError, RezError from rez.utils.logging_ import print_debug +from rez.vendor.schema.schema import Schema, Optional as SchemaOptional +from typing import Any, Generic, TypeVar, TYPE_CHECKING, ClassVar -from typing import TYPE_CHECKING, Any +from rez.utils._mypyc import mypyc_attr if TYPE_CHECKING: # this is not available in typing until 3.11, but due to __future__.annotations # we can use it without really importing it - from typing import Self - from rez.vendor.schema.schema import Schema - from rez.package_repository import PackageRepository + from typing_extensions import Self -class Resource(object, metaclass=LazyAttributeMeta): +ResourceT = TypeVar("ResourceT", bound="Resource") + +_EXTENSION_SUFFIXES = tuple(importlib.machinery.EXTENSION_SUFFIXES) + + +def _is_compiled_class(cls: type) -> bool: + """Return True if `cls` is defined in a compiled extension module. + + Note that this also works while the defining module is still being + initialized (i.e. at class-creation time). + + Known failure modes: + + - Classes synthesized at runtime whose ``__module__`` names a compiled + module are misclassified as compiled; any injection attempted on them is + silently skipped (``setattr`` on a native mypyc class raises + ``TypeError``, so the skip is intentional, but the misclassification + means the caller may not realise the class was runtime-synthesized). + - If ``__file__`` is absent (e.g. built-in modules) or the module is + ``__main__``, the function falls back to returning ``False`` (interpreted). + A wrong answer in that direction fails loudly: ``setattr`` on a compiled + class raises ``TypeError`` at class-creation time rather than silently + applying a broken injection. + """ + module = sys.modules.get(cls.__module__) + filename: str | None = getattr(module, "__file__", None) if module is not None else None + return filename is not None and filename.endswith(_EXTENSION_SUFFIXES) + + +# class Resource(object, metaclass=LazyAttributeMeta): + +@mypyc_attr(allow_interpreted_subclasses=True) +class Resource(object): """Abstract base class for a data resource. A resource is an object uniquely identified by a 'key' (the resource type), @@ -80,18 +112,67 @@ class Resource(object, metaclass=LazyAttributeMeta): `validated_data` function, and test full validation using `validate_data`. """ #: Unique identifier of the resource type. - key: str = None + key: ClassVar[str] # type: ignore[assignment] #: Schema for the resource data. #: Must validate a dict. Can be None, in which case the resource does #: not load any data. - schema: Schema | None = None + schema: ClassVar[Schema | None] = None #: The exception type to raise on key validation failure. - schema_error = Exception + schema_error: ClassVar[type[RezError]] = RezError + + def __init_subclass__(cls, **kwargs: Any) -> None: + """Add lazily-validated attributes for each key in ``cls.schema``. + + This replicates the behavior of the ``LazyAttributeMeta`` metaclass, + which cannot be used here because mypyc does not support custom + metaclasses on compiled classes. Compiled (native) subclasses have + their schema attributes explicitly written out instead (see the + "AUTO-GENERATED METHODS" sections, created with + ``rez.utils.data_utils.write_module_dynamic_members``), so they are + skipped here; interpreted subclasses get the attributes injected at + class-creation time, exactly as the metaclass did. + """ + super().__init_subclass__(**kwargs) + + # use a `type`-typed alias for dunder access, which mypyc disallows + # on the native class object + klass: type = cls + + if _is_compiled_class(klass): + # a compiled subclass: attributes are explicitly generated + return + + schema = vars(klass).get("schema") + if not schema: + return + + from rez.utils.data_utils import LazyAttributeMeta + + for key, key_schema in schema._schema.items(): + optional = isinstance(key, SchemaOptional) + while isinstance(key, Schema): + key = key._schema + if not isinstance(key, str): + continue + + if hasattr(klass, key): + attr = "_%s" % key + if hasattr(klass, attr): + # both forms already provided - eg by the explicitly + # generated members of a compiled parent class, whose + # getters resolve the key's schema at runtime via + # `_get_item`, so they remain valid for this subclass's + # schema. + continue + else: + attr = key - if TYPE_CHECKING: - # all Resources that are acquired using PackageRepository.get_resource - # have this attribute added to them - _repository: PackageRepository + prop = LazyAttributeMeta._make_getter(key, attr, optional, key_schema) + # functools.cached_property requires __set_name__, which python + # only calls for descriptors present at class creation; this is a + # post-creation setattr, so call it explicitly + prop.__set_name__(klass, attr) + setattr(klass, attr, prop) @classmethod def normalize_variables(cls, variables: dict[str, Any]) -> dict[str, Any]: @@ -102,6 +183,65 @@ def normalize_variables(cls, variables: dict[str, Any]) -> dict[str, Any]: def __init__(self, variables: dict[str, Any] | None = None) -> None: self.variables = self.normalize_variables(variables or {}) + @cached_property + def _schema_dict(self) -> dict[str, Any]: + """Unwrap the Schema into a dict where keys are strings""" + assert self.schema is not None + d = {} + for key, value in self.schema._schema.items(): + while isinstance(key, Schema): + key = key._schema + if isinstance(key, str): + d[key] = value + return d + + @cached_property + def _schema_keys(self) -> frozenset[str]: + if self.schema: + return frozenset(self._schema_dict.keys()) + else: + return frozenset() + + def validate_data(self) -> None: + self.validated_data() + + def validated_data(self) -> dict[str, Any] | None: + if self.schema: + d = {} + for key in self._schema_keys: + d[key] = getattr(self, key) + + # arbitrary keys + if self._data: + akeys = set(self._data.keys()) - set(d.keys()) + for akey in akeys: + d[akey] = self._data[akey] + + return d + else: + return None + + def _validate_key_impl(self, key: str, attr: Any, schema: Any) -> Any: + schema_ = schema if isinstance(schema, Schema) else Schema(schema) + try: + return schema_.validate(attr) + except Exception as e: + raise self.schema_error("Validation of key %r failed: " + "%s" % (key, str(e))) + + def _get_item(self, key: str, optional: bool) -> Any: + if self._data is None or key not in self._data: + if optional: + return None + raise self.schema_error("Required key is missing: %r" % key) + + attr = self._data[key] + key_schema = self._schema_dict[key] + if hasattr(self, "_validate_key"): + return self._validate_key(key, attr, key_schema) + else: + return self._validate_key_impl(key, attr, key_schema) + @cached_property def handle(self) -> ResourceHandle: """Get the resource handle.""" @@ -109,12 +249,15 @@ def handle(self) -> ResourceHandle: @cached_property def _data(self) -> dict[str, Any] | None: + from rez.config import config + if not self.schema: - return None + data = None + else: + data = self._load() + if config.debug("resources"): + print_debug("Loaded resource: %s" % str(self)) - data = self._load() - if config.debug("resources"): - print_debug("Loaded resource: %s" % str(self)) return data def get(self, key: str, default: Any | None = None) -> Any | None: @@ -130,10 +273,12 @@ def __repr__(self) -> str: def __hash__(self) -> int: return hash((self.__class__, self.handle)) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, Resource): + return NotImplemented return (self.handle == other.handle) - def _load(self) -> dict[str, Any]: + def _load(self) -> dict[str, Any] | None: """Load the data associated with the resource. You are not expected to cache this data - the resource system does this @@ -155,6 +300,7 @@ class ResourceHandle(object): A handle uniquely identifies a resource. A handle can be stored and used with a `ResourcePool` to retrieve the same resource at a later date. """ + def __init__(self, key: str, variables: dict[str, Any] | None = None) -> None: self.key = key self.variables = variables or {} @@ -191,7 +337,9 @@ def __str__(self) -> str: def __repr__(self) -> str: return "%s(%r, %r)" % (self.__class__.__name__, self.key, self.variables) - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, ResourceHandle): + return NotImplemented return (self.key == other.key) and (self.variables == other.variables) def __ne__(self, other: object) -> bool: @@ -201,6 +349,7 @@ def __hash__(self) -> int: return hash(self._hashable_repr()) +@mypyc_attr(allow_interpreted_subclasses=True) class ResourcePool(object): """A resource pool. @@ -209,6 +358,7 @@ class ResourcePool(object): resources are created via some factory class, which first checks for the existence of the resource before creating one from a pool. """ + def __init__(self, cache_size: int | None = None) -> None: self.resource_classes: dict[str, type[Resource]] = {} cache = lru_cache(maxsize=cache_size) @@ -238,7 +388,7 @@ def get_resource_from_handle(self, resource_handle: ResourceHandle) -> Resource: def clear_caches(self) -> None: self.cached_get_resource.cache_clear() - def get_resource_class(self, resource_key) -> type[Resource]: + def get_resource_class(self, resource_key: str) -> type[Resource]: resource_class = self.resource_classes.get(resource_key) if resource_class is None: raise ResourceError("Error getting resource from pool: Unknown " @@ -250,7 +400,9 @@ def _get_resource(self, resource_handle: ResourceHandle) -> Resource: return resource_class(resource_handle.variables) -class ResourceWrapper(object, metaclass=AttributeForwardMeta): +# class ResourceWrapper(Generic[ResourceT], metaclass=AttributeForwardMeta): +@mypyc_attr(allow_interpreted_subclasses=True) +class ResourceWrapper(Generic[ResourceT]): """An object that wraps a resource instance. A resource wrapper is useful for two main reasons. First, we can wrap @@ -268,13 +420,42 @@ class ResourceWrapper(object, metaclass=AttributeForwardMeta): the resource that you want to expose in the wrapper. The `schema_keys` function is provided to help get a list of keys from a resource schema. """ - keys = None + keys: set[str] | None = None + + def __init_subclass__(cls, **kwargs: Any) -> None: + """Add forwarding properties for each attribute named in ``cls.keys``. + + This replicates the behavior of the ``AttributeForwardMeta`` metaclass, + which cannot be used here because mypyc does not support custom + metaclasses on compiled classes. Compiled (native) subclasses have + their forwarded attributes explicitly written out instead (see the + "AUTO-GENERATED METHODS" sections, created with + ``rez.utils.data_utils.write_module_dynamic_members``), so they are + skipped here; interpreted subclasses get the attributes injected at + class-creation time, exactly as the metaclass did. + """ + super().__init_subclass__(**kwargs) + + # use a `type`-typed alias for dunder access, which mypyc disallows + # on the native class object + klass: type = cls + + if _is_compiled_class(klass): + # a compiled subclass: attributes are explicitly generated + return + + from rez.utils.data_utils import AttributeForwardMeta + + attr_info = AttributeForwardMeta._get_new_attrs( + dict(vars(klass)), klass.__mro__[1:]) + for key in sorted(attr_info): + setattr(klass, key, AttributeForwardMeta._make_forwarder(key)) - def __init__(self, resource: Resource) -> None: + def __init__(self, resource: ResourceT) -> None: self.wrapped = resource @property - def resource(self) -> Resource: + def resource(self) -> ResourceT: return self.wrapped @property @@ -293,9 +474,11 @@ def validate_data(self) -> None: # provided by LazyAttributeMeta metaclass self.resource.validate_data() # type: ignore[attr-defined] - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, ResourceWrapper): + return NotImplemented return ( - self.__class__ == other.__class__ + self.__class__ is other.__class__ and self.resource == other.resource ) diff --git a/src/rez/utils/scope.py b/src/rez/utils/scope.py index 517031c4e3..0299883fdb 100644 --- a/src/rez/utils/scope.py +++ b/src/rez/utils/scope.py @@ -4,17 +4,17 @@ from __future__ import annotations -from rez.utils.formatting import StringFormatMixin, StringFormatType +from rez.utils.formatting import ObjectStringFormatter, StringFormatType from collections import UserDict import sys from typing import cast, Any, TYPE_CHECKING if TYPE_CHECKING: - from typing import Self + from typing_extensions import Self -class RecursiveAttribute(UserDict, StringFormatMixin): +class RecursiveAttribute(UserDict): """An object that can have new attributes added recursively:: >>> a = RecursiveAttribute() @@ -36,14 +36,39 @@ class RecursiveAttribute(UserDict, StringFormatMixin): >>> a.new = True AttributeError: 'RecursiveAttribute' object has no attribute 'new' """ - format_expand = StringFormatType.unchanged + format_expand: ClassVar[StringFormatType] = StringFormatType.unchanged + format_pretty: ClassVar[bool] = True - def __init__(self, data=None, read_only: bool = False) -> None: + def __init__(self, data: dict[str, Any] | None = None, read_only: bool = False) -> None: self.__dict__.update(dict(data={}, read_only=read_only)) self._update(data or {}) - def __getattr__(self, attr): - def _noattrib(): + def format(self, s: str, pretty: bool | None = None, + expand: StringFormatType | None = None) -> str: + """Format a string. + + Args: + s (str): String to format, eg "hello {name}" + pretty (bool): If True, references to non-string attributes such as + lists are converted to basic form, with characters such as + brackets and parenthesis removed. If None, defaults to the + object's 'format_pretty' attribute. + expand (`StringFormatType`): Expansion mode. If None, will default + to the object's 'format_expand' attribute. + + Returns: + The formatting string. + """ + if pretty is None: + pretty = self.format_pretty + if expand is None: + expand = self.format_expand + + formatter = ObjectStringFormatter(self, pretty=pretty, expand=expand) + return formatter.format(s) + + def __getattr__(self, attr: str) -> str | RecursiveAttribute: + def _noattrib() -> NoReturn: raise AttributeError("'%s' object has no attribute '%s'" % (self.__class__.__name__, attr)) d = self.__dict__ @@ -207,6 +232,7 @@ class ScopeContext(object): and the assigned properties will be merged. If the same property is set multiple times, it will be overwritten. """ + def __init__(self) -> None: self.scopes = {} self.scope_stack = [_Scope()] diff --git a/src/rez/utils/sourcecode.py b/src/rez/utils/sourcecode.py index e036eabe40..4cdc55da1d 100644 --- a/src/rez/utils/sourcecode.py +++ b/src/rez/utils/sourcecode.py @@ -5,7 +5,7 @@ from __future__ import annotations from rez.utils.formatting import indent -from rez.utils.data_utils import cached_property +from functools import cached_property from rez.utils.logging_ import print_debug from rez.util import load_module_from_file from inspect import getsourcelines @@ -130,6 +130,7 @@ def copy(self) -> SourceCode[T]: return other def _init_from_func(self) -> None: + assert self.func is not None self.funcname = self.func.__name__ self.decorators = getattr(self.func, "_decorators", []) diff --git a/src/rez/utils/which.py b/src/rez/utils/which.py index 14c01412bf..c2f300f317 100644 --- a/src/rez/utils/which.py +++ b/src/rez/utils/which.py @@ -46,12 +46,12 @@ def _access_check(fn, mode): path = env.get("PATH", os.defpath) if not path: return None - path = path.split(os.pathsep) + paths = path.split(os.pathsep) if iswin: # The current directory takes precedence on Windows - if not dirname and os.curdir not in path: - path.insert(0, os.curdir) + if not dirname and os.curdir not in paths: + paths.insert(0, os.curdir) # PATHEXT is necessary to check on Windows pathext = env.get("PATHEXT", _default_pathext).split(os.pathsep) @@ -59,7 +59,7 @@ def _access_check(fn, mode): # iterate over paths seen = set() - for dir_ in path: + for dir_ in paths: normdir = os.path.normcase(dir_) # On windows the system paths might contain %systemroot% diff --git a/src/rez/vendor/schema/schema.py b/src/rez/vendor/schema/schema.py index 24f07b4121..b5eae57170 100644 --- a/src/rez/vendor/schema/schema.py +++ b/src/rez/vendor/schema/schema.py @@ -1,21 +1,25 @@ +from __future__ import annotations + # REZ: added a .rez to version # REZ: added date to version to indicate a non-official release __version__ = '0.3.1.2015-03-04.rez' # REZ: added a __revision__ attr __revision__ = '916ba05e22b7b370b3586f97c40695e7b9e7fe33' +from typing import Any + class SchemaError(Exception): """Error during Schema validation.""" - def __init__(self, autos, errors): - self.autos = autos if type(autos) is list else [autos] - self.errors = errors if type(errors) is list else [errors] + def __init__(self, autos: str | None | list[str | None], errors: str | list[str]): + self.autos: list[str | None] = autos if isinstance(autos, list) else [autos] + self.errors: list[str] = errors if isinstance(errors, list) else [errors] Exception.__init__(self, self.code) @property - def code(self): + def code(self) -> str: def uniq(seq): seen = set() seen_add = seen.add @@ -32,14 +36,14 @@ class And(object): def __init__(self, *args, **kw): self._args = args assert list(kw) in (['error'], []) - self._error = kw.get('error') + self._error: str | None = kw.get('error') def __repr__(self): # REZ: Switched to use a map operation instead of list comprehension. return '%s(%s)' % (self.__class__.__name__, ', '.join(map(repr, self._args))) - def validate(self, data): + def validate(self, data: Any) -> Any: for s in [Schema(s, error=self._error) for s in self._args]: data = s.validate(data) return data @@ -47,7 +51,7 @@ def validate(self, data): class Or(And): - def validate(self, data): + def validate(self, data: Any) -> Any: x = SchemaError([], []) for s in [Schema(s, error=self._error) for s in self._args]: try: @@ -81,7 +85,7 @@ def validate(self, data): COMPARABLE, CALLABLE, VALIDATOR, TYPE, DICT, ITERABLE = range(6) -def priority(s): +def priority(s) -> list[int]: """Return priority for a given object.""" # REZ: Previously this value was calculated in place many times which is # expensive. Do it once early. @@ -114,7 +118,7 @@ def __init__(self, schema, error=None): def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self._schema) - def validate(self, data): + def validate(self, data: Any): s = self._schema # REZ: Previously this value was calculated in place many times which is # expensive. Do it once early. diff --git a/src/rez/version/_requirement.py b/src/rez/version/_requirement.py index efdee8f51b..89088150c9 100644 --- a/src/rez/version/_requirement.py +++ b/src/rez/version/_requirement.py @@ -5,7 +5,8 @@ from rez.version._version import Version, VersionRange from rez.version._util import _Common import re -from typing import Iterator, Iterable +from typing import Iterator, Iterable, ClassVar +from rez.utils._mypyc import mypyc_attr class VersionedObject(_Common): @@ -18,18 +19,20 @@ class VersionedObject(_Common): Note that ``-``, ``@`` or ``#`` can be used as the seperator between object name and version, however this is purely cosmetic. ``foo-1`` is the same as ``foo@1``. """ - sep_regex_str = r'[-@#]' - sep_regex = re.compile(sep_regex_str) + sep_regex: ClassVar[re.Pattern[str]] = re.compile(r'[-@#]') def __init__(self, s: str) -> None: """ Args: s (str): """ - self.name_ = None - self.version_ = None + self.name_: str + self.version_: Version self.sep_ = '-' + if s is None: + # this is a special case in VersionedObject.construct, but name and version_ + # are always set. return m = self.sep_regex.search(s) @@ -104,6 +107,7 @@ def __str__(self) -> str: return self.name_ + sep_str + ver_str +@mypyc_attr(allow_interpreted_subclasses=True) class Requirement(_Common): """ Defines a requirement for an object. For example, ``foo-5+`` means that you @@ -136,7 +140,7 @@ class Requirement(_Common): - ``foo<3`` - ``foo==1.0.1`` """ - sep_regex = re.compile(r'[-@#=<>]') + sep_regex: ClassVar[re.Pattern[str]] = re.compile(r'[-@#=<>]') def __init__(self, s: str | None, invalid_bound_error: bool = True) -> None: """ @@ -145,11 +149,13 @@ def __init__(self, s: str | None, invalid_bound_error: bool = True) -> None: invalid_bound_error (bool): If True, raise :exc:`.VersionError` if an impossible range is given, such as ``3+<2``. """ - self.name_ = None - self.range_ = None + # there are two constructors where Requirement(None) is called, but they + # both set self.name, so we do not set its value to None here. + self.name_ = "" + self.range_: VersionRange | None = None self.negate_ = False self.conflict_ = False - self._str = None + self._str: str | None = None self.sep_ = '-' if s is None: return @@ -213,6 +219,8 @@ def range(self) -> VersionRange: Returns: VersionRange: """ + if self.range_ is None: + raise ValueError("Range is None. Did you mean self.range_ ?") return self.range_ @property @@ -255,23 +263,25 @@ def conflicts_with(self, other: Requirement | VersionedObject) -> bool: bool: """ if isinstance(other, Requirement): - if (self.name_ != other.name_) or (self.range is None) \ - or (other.range is None): + if (self.name_ != other.name_) or (self.range_ is None) \ + or (other.range_ is None): return False elif self.conflict: return False if other.conflict \ - else self.range_.issuperset(other.range_) + else self.range.issuperset(other.range) elif other.conflict: - return other.range_.issuperset(self.range_) + return other.range.issuperset(self.range) else: - return not self.range_.intersects(other.range_) - else: # VersionedObject - if (self.name_ != other.name_) or (self.range is None): + return not self.range.intersects(other.range) + elif isinstance(other, VersionedObject): + if (self.name_ != other.name_) or (self.range_ is None): return False if self.conflict: - return (other.version_ in self.range_) + return (other.version_ in self.range) else: - return (other.version_ not in self.range_) + return (other.version_ not in self.range) + else: + raise TypeError def merged(self, other: Requirement) -> Requirement | None: """Merge two requirements. @@ -300,14 +310,14 @@ def _r(r_: Requirement) -> Requirement: r.sep_ = r_.sep_ return r - if self.range is None: + if self.range_ is None: return other - elif other.range is None: + elif other.range_ is None: return self elif self.conflict: if other.conflict: r = _r(self) - r.range_ = self.range_ | other.range_ + r.range_ = self.range | other.range r.negate_ = (self.negate_ and other.negate_ and not r.range_.is_any()) return r @@ -320,7 +330,7 @@ def _r(r_: Requirement) -> Requirement: r.range_ = range_ return r elif other.conflict: - range_ = self.range_ - other.range_ + range_ = self.range - other.range if range_ is None: return None else: @@ -328,7 +338,7 @@ def _r(r_: Requirement) -> Requirement: r.range_ = range_ return r else: - range_ = self.range_ & other.range_ + range_ = self.range & other.range if range_ is None: return None else: @@ -356,6 +366,7 @@ def __str__(self) -> str: if self.negate_ or range_ is None: range_ = ~range_ if range_ else VersionRange() + assert range_ is not None if not range_.is_any(): range_str = str(range_) if range_str[0] not in ('=', '<', '>'): diff --git a/src/rez/version/_util.py b/src/rez/version/_util.py index 26d1101d42..e4c0f6449e 100644 --- a/src/rez/version/_util.py +++ b/src/rez/version/_util.py @@ -4,6 +4,7 @@ from itertools import groupby from typing import Iterable, Iterator, TypeVar +from rez.utils._mypyc import mypyc_attr T = TypeVar("T") @@ -16,6 +17,7 @@ class ParseException(Exception): pass +@mypyc_attr(allow_interpreted_subclasses=True) class _Common(object): def __str__(self) -> str: raise NotImplementedError diff --git a/src/rez/version/_version.py b/src/rez/version/_version.py index a8ec841d13..e09c3a46fa 100644 --- a/src/rez/version/_version.py +++ b/src/rez/version/_version.py @@ -10,17 +10,20 @@ import copy import string import re -from typing import cast, Any, Callable, Iterable, TypeVar, TYPE_CHECKING, overload +from typing import cast, Any, Callable, ClassVar, Generic, Iterable, TypeVar, TYPE_CHECKING, Final, overload +from rez.utils._mypyc import mypyc_attr if TYPE_CHECKING: from typing_extensions import Self +T = TypeVar("T") CallableT = TypeVar("CallableT", bound=Callable) re_token = re.compile(r"[a-zA-Z0-9_]+") +@mypyc_attr(allow_interpreted_subclasses=True) class _Comparable(_Common): def __gt__(self, other: _Comparable) -> bool: return not (self < other or self == other) @@ -72,6 +75,7 @@ class VersionToken(_Comparable): Version tokens are only allowed to contain alphanumerics (any case) and underscores. """ + def __init__(self, token: str) -> None: """ Args: @@ -109,6 +113,8 @@ def __lt__(self, other: object) -> bool: return self.less_than(other) def __eq__(self, other: object) -> bool: + if not isinstance(other, VersionToken): + return NotImplemented return (not self < other) and (not other < self) @@ -117,6 +123,7 @@ class NumericToken(VersionToken): Version token supporting numbers only. Padding is ignored. """ + def __init__(self, token: str) -> None: if not token.isdigit(): raise VersionError("Invalid version token: '%s'" % token) @@ -154,6 +161,7 @@ def next(self) -> NumericToken: class _SubToken(_Comparable): """Used internally by AlphanumericVersionToken.""" + def __init__(self, s: str) -> None: self.s = s self.n = int(s) if s.isdigit() else None @@ -201,12 +209,13 @@ class AlphanumericVersionToken(VersionToken): - ``alpha`` < ``alpha3`` - ``gamma33`` < ``33gamma`` """ - numeric_regex = re.compile("[0-9]+") - regex = re.compile(r"[a-zA-Z0-9_]+\Z") + numeric_regex: ClassVar[re.Pattern[str]] = re.compile("[0-9]+") + regex: ClassVar[re.Pattern[str]] = re.compile(r"[a-zA-Z0-9_]+\Z") def __init__(self, token: str | None) -> None: if token is None: - self.subtokens = None + # this is a special case used in __next__, and subtokens is always set there + pass elif not self.regex.match(token): raise VersionError("Invalid version token: '%s'" % token) else: @@ -294,7 +303,7 @@ class Version(_Comparable): The empty version ``''`` is the smallest possible version, and can be used to represent an unversioned resource. """ - inf = None + inf: ClassVar[Version] def __init__(self, ver_str: str | None = '', make_token: Callable[[str], VersionToken] = AlphanumericVersionToken) -> None: @@ -411,6 +420,9 @@ def as_tuple(self) -> tuple[str, ...]: Returns: tuple[str]: """ + if self.tokens is None: + # Version.inf + return () return tuple(map(str, self.tokens)) def __len__(self) -> int: @@ -438,6 +450,9 @@ def __eq__(self, other: object) -> bool: return isinstance(other, Version) and self.tokens == other.tokens def __lt__(self, other: object) -> bool: + if not isinstance(other, Version): + return NotImplemented + if self.tokens is None: return False elif other.tokens is None: @@ -464,7 +479,7 @@ def __str__(self) -> str: class _LowerBound(_Comparable): - min = None + min: ClassVar[_LowerBound] def __init__(self, version: Version, inclusive: bool) -> None: self.version = version @@ -502,7 +517,7 @@ def contains_version(self, version: Version) -> bool: class _UpperBound(_Comparable): - inf = None + inf: ClassVar[_UpperBound] def __init__(self, version: Version, inclusive: bool) -> None: self.version = version @@ -539,7 +554,7 @@ def contains_version(self, version: Version) -> bool: class _Bound(_Comparable): - any = None + any: ClassVar[_Bound] def __init__(self, lower: _LowerBound | None = None, upper: _UpperBound | None = None, @@ -637,14 +652,97 @@ def fn_(self: Any) -> Any: print(" %-17s= %s" % (key, value)) print(" %-17s= %s" % ("bounds", self.bounds)) return result - return fn_ # type: ignore[return-value] +# The regular expression for a version - one or more version tokens +# followed by a non-capturing group of version separator followed by +# one or more version tokens. +# +# Note that this assumes AlphanumericVersionToken-based versions! +# +# TODO - Would be better to have `VersionRange` keep a static dict of +# parser instances, per token class type. We would add a 'regex' static +# string to each token class, and that could be used to construct +# `version_group` as below. We need to keep a dict of these parser instances, +# to avoid recompiling the large regex every time a version range is +# instantiated. In the cpp port this would be simpler - VersionRange could +# just have a static parser that is instantiated when the version range +# template class is instantiated. +# +version_group = r"([0-9a-zA-Z_]+(?:[.-][0-9a-zA-Z_]+)*)" + +version_range_regex = ( + # Match a version number (e.g. 1.0.0) + r" ^(?P{version_group})$" + "|" + # Or match an exact version number (e.g. ==1.0.0) + " ^(?P" + " ==" # Required == operator + " (?P{version_group})?" + " )$" + "|" + # Or match an inclusive bound (e.g. 1.0.0..2.0.0) + " ^(?P" + " (?P{version_group})?" + r" \.\." # Required .. operator + " (?P{version_group})?" + " )$" + "|" + # Or match a lower bound (e.g. 1.0.0+) + " ^(?P" + " (?P>|>=)?" # Bound is exclusive? + " (?P{version_group})?" + r" (?(lower_bound_prefix)|\+)" # + only if bound is not exclusive + " )$" + "|" + # Or match an upper bound (e.g. <=1.0.0) + " ^(?P" + " (?P<(?={version_group})|<=)?" # Bound is exclusive? + " (?P{version_group})?" + " )$" + "|" + # Or match a range in ascending order (e.g. 1.0.0+<2.0.0) + " ^(?P" + " (?P" + " (?P>|>=)?" # Lower bound is exclusive? + " (?P{version_group})?" + r" (?(range_lower_asc_prefix)|\+)?" # + only if lower bound is not exclusive + " )(?P" + " (?(range_lower_asc_version),?|)" # , only if lower bound is found + " (?P<(?={version_group})|<=)" # <= only if followed by a version group + " (?P{version_group})?" + " )" + " )$" + "|" + # Or match a range in descending order (e.g. <=2.0.0,1.0.0+) + " ^(?P" + " (?P" + " (?P<|<=)?" # Upper bound is exclusive? + " (?P{version_group})?" + r" (?(range_upper_desc_prefix)|\+)?" # + only if upper bound is not exclusive + " )(?P" + " (?(range_upper_desc_version),|)" # Comma is not optional because we don't want + # to recognize something like "<4>3" + " (?P<(?={version_group})|>=?)" # >= or > only if followed + # by a version group + " (?P{version_group})?" + " )" + " )$" +).format(version_group=version_group) + +_debug_parser = False # set to True to enable parser debugging + + class _VersionRangeParser(object): - debug = False # set to True to enable parser debugging - re_flags = (re.VERBOSE | re.DEBUG) if debug else re.VERBOSE + # set to True to enable parser debugging (see `_action`, which reads + # `self.debug`). Kept as a class attribute so it resolves on compiled + # (native) instances, which have no instance __dict__. + debug: ClassVar[bool] = _debug_parser + + regex: ClassVar[re.Pattern[str]] = re.compile( + version_range_regex, (re.VERBOSE | re.DEBUG) if _debug_parser else re.VERBOSE) # The regular expression for a version - one or more version tokens # followed by a non-capturing group of version separator followed by @@ -661,68 +759,6 @@ class _VersionRangeParser(object): # just have a static parser that is instantiated when the version range # template class is instantiated. # - version_group = r"([0-9a-zA-Z_]+(?:[.-][0-9a-zA-Z_]+)*)" - - version_range_regex = ( - # Match a version number (e.g. 1.0.0) - r" ^(?P{version_group})$" - "|" - # Or match an exact version number (e.g. ==1.0.0) - " ^(?P" - " ==" # Required == operator - " (?P{version_group})?" - " )$" - "|" - # Or match an inclusive bound (e.g. 1.0.0..2.0.0) - " ^(?P" - " (?P{version_group})?" - r" \.\." # Required .. operator - " (?P{version_group})?" - " )$" - "|" - # Or match a lower bound (e.g. 1.0.0+) - " ^(?P" - " (?P>|>=)?" # Bound is exclusive? - " (?P{version_group})?" - r" (?(lower_bound_prefix)|\+)" # + only if bound is not exclusive - " )$" - "|" - # Or match an upper bound (e.g. <=1.0.0) - " ^(?P" - " (?P<(?={version_group})|<=)?" # Bound is exclusive? - " (?P{version_group})?" - " )$" - "|" - # Or match a range in ascending order (e.g. 1.0.0+<2.0.0) - " ^(?P" - " (?P" - " (?P>|>=)?" # Lower bound is exclusive? - " (?P{version_group})?" - r" (?(range_lower_asc_prefix)|\+)?" # + only if lower bound is not exclusive - " )(?P" - " (?(range_lower_asc_version),?|)" # , only if lower bound is found - " (?P<(?={version_group})|<=)" # <= only if followed by a version group - " (?P{version_group})?" - " )" - " )$" - "|" - # Or match a range in descending order (e.g. <=2.0.0,1.0.0+) - " ^(?P" - " (?P" - " (?P<|<=)?" # Upper bound is exclusive? - " (?P{version_group})?" - r" (?(range_upper_desc_prefix)|\+)?" # + only if upper bound is not exclusive - " )(?P" - " (?(range_upper_desc_version),|)" # Comma is not optional because we don't want - # to recognize something like "<4>3" - " (?P<(?={version_group})|>=?)" # >= or > only if followed - # by a version group - " (?P{version_group})?" - " )" - " )$" - ).format(version_group=version_group) - - regex = re.compile(version_range_regex, re_flags) def __init__(self, input_string: str, make_token: Callable[[str], VersionToken], @@ -922,6 +958,7 @@ class VersionRange(_Comparable): valid version range syntax. For example, ``>`` is a valid range - read like ``>''``, it means ``any version greater than the empty version``. """ + def __init__(self, range_str: str | None = '', make_token: Callable[[str], VersionToken] = AlphanumericVersionToken, invalid_bound_error: bool = True) -> None: @@ -943,12 +980,12 @@ def __init__(self, range_str: str | None = '', parser = _VersionRangeParser(range_str, make_token, invalid_bound_error=invalid_bound_error) bounds = parser.bounds - except ParseException as e: + except ParseException as perr: raise VersionError("Syntax error in version range '%s': %s" - % (range_str, str(e))) - except VersionError as e: + % (range_str, str(perr))) + except VersionError as verr: raise VersionError("Invalid version range '%s': %s" - % (range_str, str(e))) + % (range_str, str(verr))) if bounds: self.bounds = self._union(bounds) @@ -1208,9 +1245,9 @@ def contains_version(self, version: Version) -> bool: return False - def iter_intersect_test(self, iterable: Iterable[Any], - key: Callable[[Any], Version] | None = None, - descending: bool = False) -> _ContainsVersionIterator[Any]: + def iter_intersect_test(self, iterable: Iterable[T], + key: Callable[[T], Version] | None = None, + descending: bool = False) -> _ContainsVersionIterator[T]: """Performs containment tests on a sorted list of versions. This is more optimal than performing separate containment tests on a @@ -1231,9 +1268,9 @@ def iter_intersect_test(self, iterable: Iterable[Any], """ return _ContainsVersionIterator(self, iterable, key, descending) - def iter_intersecting(self, iterable: Iterable[Any], - key: Callable[[Any], Version] | None = None, - descending: bool = False) -> _ContainsVersionIterator[Any]: + def iter_intersecting(self, iterable: Iterable[T], + key: Callable[[T], Version] | None = None, + descending: bool = False) -> _ContainsVersionIterator[T]: """Like :meth:iter_intersect_test`, but returns intersections only. Returns: @@ -1243,9 +1280,9 @@ def iter_intersecting(self, iterable: Iterable[Any], self, iterable, key, descending, mode=_ContainsVersionIterator.MODE_INTERSECTING ) - def iter_non_intersecting(self, iterable: Iterable[Any], - key: Callable[[Any], Version] | None = None, - descending: bool = False) -> _ContainsVersionIterator[Any]: + def iter_non_intersecting(self, iterable: Iterable[T], + key: Callable[[T], Version] | None = None, + descending: bool = False) -> _ContainsVersionIterator[T]: """Like :meth:`iter_intersect_test`, but returns non-intersections only. Returns: @@ -1361,13 +1398,16 @@ def _union(cls, bounds: list[_Bound]) -> list[_Bound]: start = 0 for i, bound in enumerate(bounds_): - if i and ((bound.lower.version > upper.version) - or ((bound.lower.version == upper.version) - and (not bound.lower.inclusive) - and (not prev_bound.upper.inclusive))): - new_bound = _Bound(bounds_[start].lower, upper) - new_bounds.append(new_bound) - start = i + if i: + assert upper is not None + assert prev_bound is not None + if ((bound.lower.version > upper.version) + or ((bound.lower.version == upper.version) + and (not bound.lower.inclusive) + and (not prev_bound.upper.inclusive))): + new_bound = _Bound(bounds_[start].lower, upper) + new_bounds.append(new_bound) + start = i prev_bound = bound upper = bound.upper if upper is None else max(upper, bound.upper) @@ -1453,13 +1493,13 @@ def _intersects(cls, bounds1: list[_Bound], bounds2: list[_Bound]) -> bool: return False -class _ContainsVersionIterator(object): - MODE_INTERSECTING = 0 - MODE_NON_INTERSECTING = 2 - MODE_ALL = 3 +class _ContainsVersionIterator(Generic[T]): + MODE_INTERSECTING: Final[int] = 0 + MODE_NON_INTERSECTING: Final[int] = 2 + MODE_ALL: Final[int] = 3 - def __init__(self, range_: VersionRange, iterable: Iterable[Any], - key: Callable[[Any], Version] | None = None, + def __init__(self, range_: VersionRange, iterable: Iterable[T], + key: Callable[[T], Version] | None = None, descending: bool = False, mode: int = MODE_ALL) -> None: self.mode = mode self.range_ = range_ @@ -1470,10 +1510,10 @@ def __init__(self, range_: VersionRange, iterable: Iterable[Any], self.it = iter(iterable) if key is None: # FIXME: this case seems to assume that iterable is Iterable[Version] - key = cast(Callable[[Any], Version], lambda x: x) # noqa: E731 + key = cast(Callable[[T], Version], lambda x: x) # noqa: E731 self.keyfunc = key - self.next_fn: Callable[[], tuple[bool, Any]] | Callable[[], Any] + self.next_fn: Callable[[], tuple[bool, T]] | Callable[[], T] if mode == self.MODE_ALL: self.next_fn = self._next elif mode == self.MODE_INTERSECTING: @@ -1481,16 +1521,16 @@ def __init__(self, range_: VersionRange, iterable: Iterable[Any], else: self.next_fn = self._next_non_intersecting - def __iter__(self) -> _ContainsVersionIterator[Any]: + def __iter__(self) -> _ContainsVersionIterator[T]: return self - def __next__(self) -> Any | tuple[bool, Any]: + def __next__(self) -> T | tuple[bool, T]: return self.next_fn() - def next(self) -> Any | tuple[bool, Any]: + def next(self) -> T | tuple[bool, T]: return self.next_fn() - def _next(self) -> tuple[bool, Any]: + def _next(self) -> tuple[bool, T]: value = next(self.it) if self._constant is not None: return self._constant, value @@ -1499,7 +1539,7 @@ def _next(self) -> tuple[bool, Any]: intersects = self.fn(version) return intersects, value - def _next_intersecting(self) -> Any: + def _next_intersecting(self) -> T: while True: value = next(self.it) @@ -1513,7 +1553,7 @@ def _next_intersecting(self) -> Any: if intersects: return value - def _next_non_intersecting(self) -> Any: + def _next_non_intersecting(self) -> T: while True: value = next(self.it) @@ -1529,6 +1569,7 @@ def _next_non_intersecting(self) -> Any: @property def _bound(self) -> _Bound | None: + assert self.index is not None if self.index < self.nbounds: return self.range_.bounds[self.index] else: @@ -1539,6 +1580,7 @@ def _ascending(self, version: Version) -> bool: self.index, contains = self.range_._contains_version(version) bound = self._bound if contains: + assert bound is not None if not bound.upper_bounded(): self._constant = True return True @@ -1549,6 +1591,7 @@ def _ascending(self, version: Version) -> bool: return False # there are more bound(s) ahead else: bound = self._bound + assert bound is not None j = bound.version_containment(version) if j == 0: return True @@ -1575,6 +1618,7 @@ def _descending(self, version: Version) -> bool: self.index, contains = self.range_._contains_version(version) bound = self._bound if contains: + assert bound is not None if not bound.lower_bounded(): self._constant = True return True @@ -1589,6 +1633,7 @@ def _descending(self, version: Version) -> bool: return False else: bound = self._bound + assert bound is not None j = bound.version_containment(version) if j == 0: return True @@ -1598,6 +1643,7 @@ def _descending(self, version: Version) -> bool: while self.index: self.index -= 1 bound = self._bound + assert bound is not None j = bound.version_containment(version) if j == 0: if not bound.lower_bounded(): diff --git a/src/rez/wrapper.py b/src/rez/wrapper.py index 75e401e636..a31441a3c5 100644 --- a/src/rez/wrapper.py +++ b/src/rez/wrapper.py @@ -6,7 +6,7 @@ from rez.resolved_context import ResolvedContext from rez.utils.colorize import heading, local, critical, Printer -from rez.utils.data_utils import cached_property +from functools import cached_property from rez.utils.formatting import columnise from rez.vendor import yaml from rez.vendor.yaml.error import YAMLError diff --git a/src/rezgui/objects/App.py b/src/rezgui/objects/App.py index 418d662fec..81f52a37a3 100644 --- a/src/rezgui/objects/App.py +++ b/src/rezgui/objects/App.py @@ -8,7 +8,7 @@ from rezgui import organisation_name, application_name from rez.resolved_context import ResolvedContext from rez.exceptions import ResolvedContextError -from rez.utils.data_utils import cached_property +from functools import cached_property from rez.vendor import yaml from contextlib import contextmanager import sys @@ -62,7 +62,7 @@ def load_context(self, filepath): context = ResolvedContext.load(filepath) except ResolvedContextError as e: QtWidgets.QMessageBox.critical(self.main_window, - "Failed to load context", str(e)) + "Failed to load context", str(e)) finally: QtWidgets.QApplication.restoreOverrideCursor() @@ -90,5 +90,6 @@ def execute_shell(self, context, command=None, terminal: bool = False, **Popen_a start_new_session=True, **Popen_args) + # app singleton app = App() diff --git a/src/rezplugins/package_repository/filesystem.py b/src/rezplugins/package_repository/filesystem.py index 4d35e7803d..768bf5e561 100644 --- a/src/rezplugins/package_repository/filesystem.py +++ b/src/rezplugins/package_repository/filesystem.py @@ -16,18 +16,19 @@ from rez.package_repository import PackageRepository from rez.package_resources import PackageFamilyResource, VariantResourceHelper, \ - PackageResourceHelper, package_pod_schema, \ + PackageResourceHelper, PackageRepositoryResource, package_pod_schema, \ package_release_keys, package_build_only_keys from rez.serialise import clear_file_caches, open_file_for_write, load_from_file, \ FileFormat from rez.package_serialise import dump_package_data from rez.exceptions import PackageMetadataError, ResourceError, RezSystemError, \ ConfigurationError, PackageRepositoryError -from rez.utils.resources import ResourcePool +from rez.utils.data_utils import RO_AttrDictWrapper +from rez.utils.resources import ResourcePool, ResourceHandle from rez.utils.formatting import is_valid_package_name -from rez.utils.resources import cached_property +from functools import cached_property from rez.utils.logging_ import print_warning, print_info -from rez.utils.memcached import memcached, pool_memcached_connections +from rez.utils.memcached import memcached, memcached_client from rez.utils.filesystem import make_path_writable, \ canonical_path, is_subdirectory, safe_rmtree from rez.utils.platform_ import platform_ @@ -36,12 +37,10 @@ from rez.vendor.schema.schema import Schema, Optional, And, Use, Or from rez.version import Version, VersionRange -from typing import Any, Iterator, Iterable, TYPE_CHECKING +from typing import cast, Any, ClassVar, Iterator, Iterable, Union, TYPE_CHECKING if TYPE_CHECKING: - from typing import Self - from rez.packages import Package, PackageRepositoryResourceWrapper - from rez.package_resources import PackageRepositoryResource, VariantResource + from typing_extensions import Self debug_print = config.debug_printer("resources") @@ -79,7 +78,7 @@ def check_format_version(filename: str, data: dict[str, Any]) -> None: # this is set when the package repository is instantiated, otherwise an infinite # loop is caused to to config loading this plugin, loading config ad infinitum -_settings = None +_settings: RO_AttrDictWrapper class PackageDefinitionFileMissing(PackageMetadataError): @@ -90,7 +89,8 @@ class PackageDefinitionFileMissing(PackageMetadataError): # resources # ------------------------------------------------------------------------------ -class FileSystemPackageFamilyResource(PackageFamilyResource): +class FileSystemPackageFamilyResource( + PackageFamilyResource["FileSystemPackageRepository", "FileSystemPackageResource"]): key = "filesystem.family" repository_type = "filesystem" @@ -101,35 +101,36 @@ def _uri(self) -> str: def path(self) -> str: return os.path.join(self.location, self.name) - def get_last_release_time(self) -> float: + def get_last_release_time(self) -> int: # this repository makes sure to update path mtime every time a # variant is added to the repository try: - return os.path.getmtime(self.path) + # getmtime can return int or float. we will pretend it's always int + return os.path.getmtime(self.path) # type: ignore[return-value] except OSError: return 0 def iter_packages(self) -> Iterator[FileSystemPackageResource]: # check for unversioned package if config.allow_unversioned_packages: - filepath, _ = self._repository._get_file(self.path) + filepath, _ = self.repository._get_file(self.path) if filepath: - package = self._repository.get_resource( - FileSystemPackageResource.key, + package = self.repository.get_resource( + FileSystemPackageResource, location=self.location, name=self.name) yield package return # versioned packages - for version_str in self._repository._get_version_dirs(self.path): + for version_str in self.repository._get_version_dirs(self.path): if _settings.check_package_definition_files: path = os.path.join(self.path, version_str) - if not self._repository._get_file(path)[0]: + if not self.repository._get_file(path)[0]: continue - package = self._repository.get_resource( - FileSystemPackageResource.key, + package = self.repository.get_resource( + FileSystemPackageResource, location=self.location, name=self.name, version=version_str) @@ -137,27 +138,39 @@ def iter_packages(self) -> Iterator[FileSystemPackageResource]: yield package -class FileSystemPackageResource(PackageResourceHelper): +class FileSystemPackageResource(PackageResourceHelper["FileSystemVariantResource"]): key = "filesystem.package" variant_key = "filesystem.variant" repository_type = "filesystem" schema = package_pod_schema def _uri(self) -> str: + assert self.filepath is not None return self.filepath + @property + def _data(self) -> dict[str, Any]: + data = super()._data + assert data is not None + return data + + def validated_data(self) -> dict[str, Any]: + data = super().validated_data() + assert data is not None + return data + @cached_property def parent(self) -> FileSystemPackageFamilyResource: - family = self._repository.get_resource( - FileSystemPackageFamilyResource.key, + family = self.repository.get_resource( + FileSystemPackageFamilyResource, location=self.location, name=self.name) return family @cached_property - def state_handle(self) -> float | None: + def state_handle(self) -> int | None: if self.filepath: - return os.path.getmtime(self.filepath) + return os.path.getmtime(self.filepath) # type: ignore[return-value] return None @property @@ -178,17 +191,19 @@ def path(self) -> str: path = os.path.join(path, ver_str) return path - @cached_property - def filepath(self) -> str | None: + @property + def filepath(self) -> str: return self._filepath_and_format[0] - @cached_property - def file_format(self) -> FileFormat | None: + @property + def file_format(self) -> FileFormat: return self._filepath_and_format[1] @cached_property - def _filepath_and_format(self) -> tuple[str, FileFormat] | tuple[None, None]: - return self._repository._get_file(self.path) + def _filepath_and_format(self) -> tuple[str, FileFormat]: + result = self.repository._get_file(self.path) + assert result is not None + return result def _load(self) -> dict[str, Any]: if self.filepath is None: @@ -198,7 +213,7 @@ def _load(self) -> dict[str, Any]: data = load_from_file( self.filepath, self.file_format, - disable_memcache=self._repository.disable_memcache + disable_memcache=self.repository.disable_memcache ) check_format_version(self.filepath, data) @@ -211,7 +226,7 @@ def _load(self) -> dict[str, Any]: return data # TODO: Deprecate? How could we add deprecation warnings without flooding the user? - def _load_old_formats(self): + def _load_old_formats(self) -> dict | None: data = None filepath = os.path.join(self.path, "release.yaml") @@ -240,7 +255,7 @@ def _load_old_formats(self): return data @staticmethod - def _update_changelog(file_format, data): + def _update_changelog(file_format: FileFormat, data: dict) -> dict: # this is to deal with older package releases. They can contain long # changelogs (more recent rez versions truncate before release), and # release.yaml files can contain a list-of-str changelog. @@ -273,10 +288,14 @@ class FileSystemVariantResource(VariantResourceHelper): key = "filesystem.variant" repository_type = "filesystem" + # note: do not override `_data` to assert non-None here: variant resources + # legitimately have no data of their own (VariantResourceHelper._load + # returns None) - data is forwarded from the parent package instead. + @cached_property def parent(self) -> FileSystemPackageResource: - package = self._repository.get_resource( - FileSystemPackageResource.key, + package = self.repository.get_resource( + FileSystemPackageResource, location=self.location, name=self.name, version=self.get("version")) @@ -285,7 +304,8 @@ def parent(self) -> FileSystemPackageResource: # -- 'combined' resource types -class FileSystemCombinedPackageFamilyResource(PackageFamilyResource): +class FileSystemCombinedPackageFamilyResource( + PackageFamilyResource["FileSystemPackageRepository", "FileSystemCombinedPackageResource"]): key = "filesystem.family.combined" repository_type = "filesystem" @@ -299,28 +319,36 @@ class FileSystemCombinedPackageFamilyResource(PackageFamilyResource): }) @property - def ext(self): - return self.get("ext") + def _data(self) -> dict[str, Any]: + data = super()._data + assert data is not None + return data + + @property + def ext(self) -> str: + ext = self.get("ext") + assert isinstance(ext, str) + return ext @property - def filepath(self): + def filepath(self) -> str: filename = "%s.%s" % (self.name, self.ext) return os.path.join(self.location, filename) - def _uri(self): + def _uri(self) -> str: return self.filepath - def get_last_release_time(self): + def get_last_release_time(self) -> int: try: - return os.path.getmtime(self.filepath) + return int(os.path.getmtime(self.filepath)) except OSError: return 0 def iter_packages(self) -> Iterator[FileSystemCombinedPackageResource]: # unversioned package if config.allow_unversioned_packages and not self.versions: - package = self._repository.get_resource( - FileSystemCombinedPackageResource.key, + package = self.repository.get_resource( + FileSystemCombinedPackageResource, location=self.location, name=self.name, ext=self.ext) @@ -328,27 +356,38 @@ def iter_packages(self) -> Iterator[FileSystemCombinedPackageResource]: return # versioned packages - for version in self.versions: - package = self._repository.get_resource( - FileSystemCombinedPackageResource.key, + for version in self.versions or []: + package = self.repository.get_resource( + FileSystemCombinedPackageResource, location=self.location, name=self.name, ext=self.ext, version=str(version)) yield package - def _load(self): + def _load(self) -> dict[str, Any]: # TODO: Deprecate: What is self.ext? format_ = FileFormat[self.ext] data = load_from_file( self.filepath, format_, - disable_memcache=self._repository.disable_memcache + disable_memcache=self.repository.disable_memcache ) check_format_version(self.filepath, data) return data + # -- BEGIN AUTO-GENERATED METHODS -- + @cached_property + def version_overrides(self) -> dict[VersionRange, dict] | None: + return self._get_item('version_overrides', True) + + @cached_property + def versions(self) -> list[Version] | None: + return self._get_item('versions', True) + + # -- END AUTO-GENERATED METHODS -- + class FileSystemCombinedPackageResource(PackageResourceHelper): key = "filesystem.package.combined" @@ -360,10 +399,21 @@ def _uri(self) -> str: ver_str = self.get("version", "") return "%s<%s>" % (self.parent.filepath, ver_str) + @property + def _data(self) -> dict[str, Any]: + data = super()._data + assert data is not None + return data + + def validated_data(self) -> dict[str, Any]: + data = super().validated_data() + assert data is not None + return data + @cached_property def parent(self) -> FileSystemCombinedPackageFamilyResource: - family = self._repository.get_resource( - FileSystemCombinedPackageFamilyResource.key, + family = self.repository.get_resource( + FileSystemCombinedPackageFamilyResource, location=self.location, name=self.name, ext=self.get("ext")) @@ -385,7 +435,7 @@ def iter_variants(self) -> Iterator[FileSystemCombinedVariantResource]: indexes = range(num_variants) for index in indexes: - variant = self._repository.get_resource( + variant = self.repository.get_resource( self.variant_key, location=self.location, name=self.name, @@ -395,6 +445,7 @@ def iter_variants(self) -> Iterator[FileSystemCombinedVariantResource]: yield variant def _load(self) -> dict[str, Any] | None: + assert self.parent._data is not None, "Should not be None since parent has schema" data = self.parent._data.copy() if "versions" in data: @@ -417,10 +468,14 @@ class FileSystemCombinedVariantResource(VariantResourceHelper): key = "filesystem.variant.combined" repository_type = "filesystem" + # note: do not override `_data` to assert non-None here: variant resources + # legitimately have no data of their own (VariantResourceHelper._load + # returns None) - data is forwarded from the parent package instead. + @cached_property - def parent(self) -> PackageRepositoryResource: - package = self._repository.get_resource( - FileSystemCombinedPackageResource.key, + def parent(self) -> FileSystemCombinedPackageResource: + package = self.repository.get_resource( + FileSystemCombinedPackageResource, location=self.location, name=self.name, ext=self.get("ext"), @@ -435,7 +490,10 @@ def _root(self, ignore_shortlinks: bool = False) -> str | None: # repository # ------------------------------------------------------------------------------ -class FileSystemPackageRepository(PackageRepository): +class FileSystemPackageRepository( + PackageRepository[Union[FileSystemVariantResource, FileSystemCombinedVariantResource], + Union[FileSystemPackageResource, FileSystemCombinedPackageResource], + Union[FileSystemPackageFamilyResource, FileSystemCombinedPackageFamilyResource]]): """A filesystem-based package repository. TODO: Deprecate YAML @@ -471,10 +529,11 @@ class FileSystemPackageRepository(PackageRepository): requires: - python-2.6 """ - schema_dict = {"file_lock_timeout": int, - "file_lock_dir": Or(None, str), - "file_lock_type": Or("default", "link", "mkdir", "symlink"), - "package_filenames": [str]} + schema_dict: ClassVar[dict[str, Any]] = { + "file_lock_timeout": int, + "file_lock_dir": Or(None, str), + "file_lock_type": Or("default", "link", "mkdir", "symlink"), + "package_filenames": [str]} building_prefix = ".building" ignore_prefix = ".ignore" @@ -538,7 +597,13 @@ def __init__(self, location: str, resource_pool: ResourcePool, disable_memcache: self.get_variants = lru_cache(maxsize=None)(self._get_variants) self.get_file = lru_cache(maxsize=None)(self._get_file) - # decorate with memcachemed memoizers unless told otherwise + # Memoized dir-listing callables. These are held as instance attributes + # (not method overrides) because this class is compiled by mypyc as a + # native class with no instance __dict__: assigning to a name that also + # exists as a `def` method (e.g. `setattr(self, "_get_family_dirs", ...)`) + # raises "attribute ... is read-only". The underlying implementations + # live in `_get_family_dirs_impl` / `_get_version_dirs_impl`; here we + # optionally wrap them with the memcached memoizer. if not self.disable_memcache: decorator1 = memcached( servers=config.memcached_uri if config.cache_listdir else None, @@ -546,7 +611,7 @@ def __init__(self, location: str, resource_pool: ResourcePool, disable_memcache: key=self._get_family_dirs__key, debug=config.debug_memcache ) - self._get_family_dirs = decorator1(self._get_family_dirs) + self._get_family_dirs = decorator1(self._get_family_dirs_impl) decorator2 = memcached( servers=config.memcached_uri if config.cache_listdir else None, @@ -554,46 +619,60 @@ def __init__(self, location: str, resource_pool: ResourcePool, disable_memcache: key=self._get_version_dirs__key, debug=config.debug_memcache ) - self._get_version_dirs = decorator2(self._get_version_dirs) + self._get_version_dirs = decorator2(self._get_version_dirs_impl) + else: + self._get_family_dirs = self._get_family_dirs_impl + self._get_version_dirs = self._get_version_dirs_impl def _uid(self) -> tuple: - t = ["filesystem", self.location] if os.path.exists(self.location): st = os.stat(self.location) - t.append(int(st.st_ino)) - return tuple(t) + return "filesystem", self.location, int(st.st_ino) + else: + return "filesystem", self.location - def get_package_family(self, name: str) -> PackageFamilyResource: + def get_package_family(self, name: str) -> FileSystemPackageFamilyResource | FileSystemCombinedPackageFamilyResource | None: # type: ignore[override] # noqa: E501 return self.get_family(name) - @pool_memcached_connections - def iter_package_families(self) -> Iterator[PackageFamilyResource]: - for family in self.get_families(): - yield family - - @pool_memcached_connections - def iter_packages(self, package_family_resource: PackageFamilyResource) -> Iterator[Package]: - for package in self.get_packages(package_family_resource): - yield package + # NOTE: pooling is inlined with `memcached_client()` rather than applied via + # the @pool_memcached_connections decorator: mypyc cannot compile a + # decorated generator method that overrides a base-class method (it raises + # KeyError in handle_ext_method when reconciling the override signature). + # Inlining keeps a single pooled client open for the whole iteration while + # remaining a plain, compilable generator method. + def iter_package_families(self) -> Iterator[FileSystemPackageFamilyResource | FileSystemCombinedPackageFamilyResource]: # noqa: E501 + with memcached_client(): + for family in self.get_families(): + yield family + + def iter_packages(self, package_family_resource: PackageFamilyResource + ) -> Iterator[FileSystemPackageResource | FileSystemCombinedPackageResource]: + with memcached_client(): + for package in self.get_packages(package_family_resource): + yield package - def iter_variants(self, package_resource: PackageResourceHelper) -> Iterator[VariantResource]: + def iter_variants(self, package_resource: FileSystemPackageResource | FileSystemCombinedPackageResource + ) -> Iterator[FileSystemVariantResource | FileSystemCombinedVariantResource]: for variant in self.get_variants(package_resource): yield variant - def get_parent_package_family(self, package_resource: PackageResourceHelper) -> PackageRepositoryResource: + def get_parent_package_family(self, package_resource: FileSystemPackageResource | FileSystemCombinedPackageResource + ) -> FileSystemPackageFamilyResource | FileSystemCombinedPackageFamilyResource: return package_resource.parent - def get_parent_package(self, variant_resource: VariantResource) -> PackageRepositoryResource: + def get_parent_package(self, variant_resource: FileSystemVariantResource | FileSystemCombinedVariantResource + ) -> FileSystemPackageResource | FileSystemCombinedPackageResource: return variant_resource.parent - def get_variant_state_handle(self, variant_resource: VariantResource): + def get_variant_state_handle(self, variant_resource: FileSystemVariantResource | FileSystemCombinedVariantResource + ) -> float | None: package_resource = variant_resource.parent return package_resource.state_handle - def get_last_release_time(self, package_family_resource: PackageFamilyResource): + def get_last_release_time(self, package_family_resource: FileSystemPackageFamilyResource | FileSystemCombinedPackageFamilyResource) -> int: # noqa: E501 return package_family_resource.get_last_release_time() - def get_package_from_uri(self, uri: str) -> PackageResourceHelper | None: + def get_package_from_uri(self, uri: str) -> FileSystemPackageResource | FileSystemCombinedPackageResource | None: """ Example URIs: - /svr/packages/mypkg/1.0.0/package.py @@ -629,7 +708,7 @@ def get_package_from_uri(self, uri: str) -> PackageResourceHelper | None: pkg_ver = Version(pkg_ver_str) return self.get_package(pkg_name, pkg_ver) - def get_variant_from_uri(self, uri: str) -> VariantResourceHelper | None: + def get_variant_from_uri(self, uri: str) -> FileSystemVariantResource | FileSystemCombinedVariantResource | None: """ Example URIs: - /svr/packages/mypkg/1.0.0/package.py[1] @@ -651,7 +730,7 @@ def get_variant_from_uri(self, uri: str) -> VariantResourceHelper | None: # find variant in package if variant_index_str == '': - variant_index = None + variant_index: int | str | None = None else: try: variant_index = int(variant_index_str) @@ -771,11 +850,11 @@ def remove_package_family(self, pkg_name: str, force: bool = False) -> bool: self._on_changed(pkg_name) return True - def remove_ignored_since(self, days, dry_run: bool = False, verbose: bool = False) -> int: + def remove_ignored_since(self, days: int, dry_run: bool = False, verbose: bool = False) -> int: now = int(time.time()) num_removed = 0 - def _info(msg, *nargs) -> None: + def _info(msg: str, *nargs: Any) -> None: if verbose: print_info(msg, *nargs) @@ -811,10 +890,11 @@ def _info(msg, *nargs) -> None: return num_removed - def get_resource_from_handle(self, resource_handle, verify_repo: bool = True): + def get_resource_from_handle(self, resource_handle: ResourceHandle, + verify_repo: bool = True) -> PackageRepositoryResource: if verify_repo: - repository_type = resource_handle.variables.get("repository_type") - location = resource_handle.variables.get("location") + repository_type = resource_handle.variables["repository_type"] + location = resource_handle.variables["location"] if repository_type != self.name(): raise ResourceError("repository_type mismatch - requested %r, " @@ -835,7 +915,7 @@ def get_resource_from_handle(self, resource_handle, verify_repo: bool = True): "repository location is %r " % (location, self.location)) - resource = self.pool.get_resource_from_handle(resource_handle) + resource = cast(PackageRepositoryResource, self.pool.get_resource_from_handle(resource_handle)) resource._repository = self return resource @@ -858,7 +938,10 @@ def file_lock_dir(self) -> str | None: return dirname - def pre_variant_install(self, variant_resource: VariantResourceHelper) -> None: + def pre_variant_install(self, variant_resource: VariantResourceHelper + ) -> None: + # note: variant_resource may come from another repository type (e.g. + # memory), so it must not be typed as a filesystem-specific resource if not variant_resource.version: return @@ -875,7 +958,8 @@ def pre_variant_install(self, variant_resource: VariantResourceHelper) -> None: with open(filepath, 'w'): # create empty file pass - def on_variant_install_cancelled(self, variant_resource) -> None: + def on_variant_install_cancelled(self, variant_resource: VariantResourceHelper + ) -> None: """ TODO: Currently this will not delete a newly created package version @@ -898,8 +982,12 @@ def on_variant_install_cancelled(self, variant_resource) -> None: family_path = os.path.join(self.location, variant_resource.name) self._delete_stale_build_tagfiles(family_path) - def install_variant(self, variant_resource: VariantResource, - dry_run: bool = False, overrides: dict[str, Any] | None = None) -> VariantResource: + def install_variant(self, variant_resource: VariantResourceHelper, + dry_run: bool = False, overrides: dict[str, Any] | None = None + ) -> FileSystemVariantResource | FileSystemCombinedVariantResource | None: + # note: variant_resource is typically a resource from another repository + # type (e.g. memory) that is being installed into this one, so it must + # not be typed as a filesystem-specific resource overrides = overrides or {} # Name and version overrides are a special case - they change the @@ -931,7 +1019,11 @@ def install_variant(self, variant_resource: VariantResource, if variant_resource._repository is self and \ variant_name == variant_resource.name and \ variant_version == variant_resource.version: - return variant_resource + # given that the resource belongs to this repository, it must be a + # filesystem resource type + return cast( + "FileSystemVariantResource | FileSystemCombinedVariantResource", + variant_resource) # create repo path on disk if it doesn't exist path = self.location @@ -945,7 +1037,7 @@ def install_variant(self, variant_resource: VariantResource, ) # install the variant - def _create_variant() -> VariantResource: + def _create_variant() -> FileSystemVariantResource | FileSystemCombinedVariantResource | None: return self._create_variant( variant_resource, dry_run=dry_run, @@ -960,7 +1052,7 @@ def _create_variant() -> VariantResource: return variant - def _copy(self, **kwargs) -> Self: + def _copy(self, **kwargs: Any) -> Self: """ Make a copy of the repo that does not share resources with this one. """ @@ -972,14 +1064,23 @@ def _copy(self, **kwargs) -> Self: def _lock_package(self, package_name: str, package_version: str | Version | None = None) -> Iterator[None]: from rez.vendor.lockfile import NotLocked + # note: the explicit assignments below (rather than `import ... as LockFile`) + # work around a mypyc bug, where a `from x import y` statement inside a + # generator does not assign to a pre-declared variable in the generator + # environment, resulting in an AttributeError at runtime. + LockFile: Any if _settings.file_lock_type == 'default': - from rez.vendor.lockfile import LockFile + from rez.vendor.lockfile import LockFile as _DefaultLockFile + LockFile = _DefaultLockFile elif _settings.file_lock_type == 'mkdir': - from rez.vendor.lockfile.mkdirlockfile import MkdirLockFile as LockFile + from rez.vendor.lockfile.mkdirlockfile import MkdirLockFile + LockFile = MkdirLockFile elif _settings.file_lock_type == 'link': - from rez.vendor.lockfile.linklockfile import LinkLockFile as LockFile + from rez.vendor.lockfile.linklockfile import LinkLockFile + LockFile = LinkLockFile elif _settings.file_lock_type == 'symlink': - from rez.vendor.lockfile.symlinklockfile import SymlinkLockFile as LockFile + from rez.vendor.lockfile.symlinklockfile import SymlinkLockFile + LockFile = SymlinkLockFile path = self.location @@ -1017,8 +1118,8 @@ def clear_caches(self) -> None: self.get_file.cache_clear() if not self.disable_memcache: - self._get_family_dirs.forget() - self._get_version_dirs.forget() + self._get_family_dirs.forget() # type: ignore[attr-defined] + self._get_version_dirs.forget() # type: ignore[attr-defined] # unfortunately we need to clear file cache across the board clear_file_caches() @@ -1041,7 +1142,7 @@ def _get_family_dirs__key(self) -> str: else: return str(("listdir", self.location)) - def _get_family_dirs(self) -> list[tuple[str, str | None]]: + def _get_family_dirs_impl(self) -> list[tuple[str, str | None]]: dirs: list[tuple[str, str | None]] = [] if not os.path.isdir(self.location): return dirs @@ -1066,7 +1167,7 @@ def _get_version_dirs__key(self, root: str) -> str: st = os.stat(root) return str(("listdir", root, int(st.st_ino), st.st_mtime)) - def _get_version_dirs(self, root: str) -> list[str]: + def _get_version_dirs_impl(self, root: str) -> list[str]: # Ignore a version if there is a .ignore file next to it def ignore_dir(name: str) -> bool: if self.disable_pkg_ignore: @@ -1129,17 +1230,17 @@ def ignore_dir(name: str) -> bool: def _is_valid_package_directory(self, path: str) -> bool: return bool(self._get_file(path, "package")[0]) - def _get_families(self) -> list[PackageFamilyResource]: + def _get_families(self) -> list[FileSystemPackageFamilyResource | FileSystemCombinedPackageFamilyResource]: families = [] for name, ext in self._get_family_dirs(): if ext is None: # is a directory - family = self.get_resource( - FileSystemPackageFamilyResource.key, + family: FileSystemPackageFamilyResource | FileSystemCombinedPackageFamilyResource = self.get_resource( + FileSystemPackageFamilyResource, location=self.location, name=name) else: family = self.get_resource( - FileSystemCombinedPackageFamilyResource.key, + FileSystemCombinedPackageFamilyResource, location=self.location, name=name, ext=ext) @@ -1147,7 +1248,7 @@ def _get_families(self) -> list[PackageFamilyResource]: return families - def _get_family(self, name: str) -> PackageFamilyResource | None: + def _get_family(self, name: str) -> FileSystemPackageFamilyResource | FileSystemCombinedPackageFamilyResource | None: # noqa: E501 is_valid_package_name(name, raise_error=True) if os.path.isdir(os.path.join(self.location, name)): # force case-sensitive match on pkg family dir, on case-insensitive platforms @@ -1156,13 +1257,14 @@ def _get_family(self, name: str) -> PackageFamilyResource | None: return None return self.get_resource( - FileSystemPackageFamilyResource.key, + FileSystemPackageFamilyResource, location=self.location, name=name ) else: filepath, format_ = self.get_file(self.location, package_filename=name) if filepath: + assert format_ is not None, "if filepath is not None, then format is not None" # force case-sensitive match on pkg filename, on case-insensitive platforms if not platform_.has_case_sensitive_filesystem: ext = os.path.splitext(filepath)[-1] @@ -1170,20 +1272,23 @@ def _get_family(self, name: str) -> PackageFamilyResource | None: return None return self.get_resource( - FileSystemCombinedPackageFamilyResource.key, + FileSystemCombinedPackageFamilyResource, location=self.location, name=name, ext=format_.extension ) return None - def _get_packages(self, package_family_resource: PackageFamilyResource) -> list[Package]: + def _get_packages(self, package_family_resource: FileSystemPackageFamilyResource | FileSystemCombinedPackageFamilyResource # noqa: E501 + ) -> list[FileSystemPackageResource | FileSystemCombinedPackageResource]: return [x for x in package_family_resource.iter_packages()] - def _get_variants(self, package_resource: PackageResourceHelper) -> list[VariantResource]: + def _get_variants(self, package_resource: FileSystemPackageResource | FileSystemCombinedPackageResource + ) -> list[FileSystemVariantResource | FileSystemCombinedVariantResource]: + # FIXME: PackageResourceHelper should be generic on VariantResourceHelperT return [x for x in package_resource.iter_variants()] - def _get_file(self, path: str, package_filename=None) -> tuple[str, FileFormat] | tuple[None, None]: + def _get_file(self, path: str, package_filename: str | None = None) -> tuple[str, FileFormat] | tuple[None, None]: if package_filename: package_filenames = [package_filename] else: @@ -1198,7 +1303,7 @@ def _get_file(self, path: str, package_filename=None) -> tuple[str, FileFormat] return filepath, format_ return None, None - def _create_family(self, name: str) -> PackageFamilyResource: + def _create_family(self, name: str) -> FileSystemPackageFamilyResource | FileSystemCombinedPackageFamilyResource | None: # noqa: E501 path = os.path.join(self.location, name) os.makedirs(path, exist_ok=True) @@ -1206,8 +1311,12 @@ def _create_family(self, name: str) -> PackageFamilyResource: return self.get_package_family(name) # FIXME: overrides should not default to None, it must be provided - def _create_variant(self, variant: VariantResource, dry_run: bool = False, - overrides: dict[str, Any] = None) -> VariantResource | None: + def _create_variant(self, variant: VariantResourceHelper, + dry_run: bool = False, + *, overrides: dict[str, Any] + ) -> FileSystemVariantResource | FileSystemCombinedVariantResource | None: + # note: variant may come from another repository type (e.g. memory), + # so it must not be typed as a filesystem-specific resource # special case overrides variant_name = overrides.get("name") or variant.name variant_version = overrides.get("version") or variant.version @@ -1232,9 +1341,12 @@ def _create_variant(self, variant: VariantResource, dry_run: bool = False, % family.filepath) # find the package if it already exists - existing_package: Package | None = None + existing_package: FileSystemPackageResource | None = None for package in self.iter_packages(family): + # combined-style families are rejected above, so only filesystem + # package resources can appear here + assert isinstance(package, FileSystemPackageResource) if package.version == variant_version: # during a build, the family/version dirs get created ahead of # time, which causes a 'missing package definition file' error. @@ -1264,21 +1376,22 @@ def _create_variant(self, variant: VariantResource, dry_run: bool = False, "Attempting to install a variant (%r) into an existing " "package without variants (%r)" % (variant, package)) - existing_package_data = None + existing_package_data: dict release_data = {} # Need to treat 'config' as special case. In validated data, this is # converted to a Config object. We need it as the raw dict that you'd # see in a package.py. # - def _get_package_data(pkg: PackageRepositoryResourceWrapper): - data = pkg.validated_data() + def _get_package_data(pkg: PackageResourceHelper) -> dict: + data = pkg.validated_data() or {} if hasattr(pkg, "_data"): raw_data = pkg._data else: - raw_data = pkg.resource._data + # FIXME: this seems like a real problem + raw_data = pkg.resource._data # type: ignore[attr-defined] - raw_config_data = raw_data.get('config') + raw_config_data = (raw_data or {}).get('config') data.pop("config", None) if raw_config_data: @@ -1286,7 +1399,7 @@ def _get_package_data(pkg: PackageRepositoryResourceWrapper): return data - def _remove_build_keys(obj) -> None: + def _remove_build_keys(obj: dict) -> None: for key in package_build_only_keys: obj.pop(key, None) @@ -1348,11 +1461,14 @@ def _remove_build_keys(obj) -> None: else: variant_requires = variant.variant_requires - for variant_ in self.iter_variants(existing_package): - variant_requires_ = existing_package.variants[variant_.index] - if variant_requires_ == variant_requires: - installed_variant_index = variant_.index - existing_installed_variant = variant_ + variants = existing_package.variants + if variants: + for variant_ in self.iter_variants(existing_package): + assert variant_.index is not None + variant_requires_ = variants[variant_.index] + if variant_requires_ == variant_requires: + installed_variant_index = variant_.index + existing_installed_variant = variant_ if existing_installed_variant: debug_print( @@ -1361,6 +1477,7 @@ def _remove_build_keys(obj) -> None: ) if dry_run: + # these results may be None, which only occurs during dry_run if not package_changed: return existing_installed_variant else: diff --git a/src/rezplugins/package_repository/memory.py b/src/rezplugins/package_repository/memory.py index 1035298738..ba72ccaf01 100644 --- a/src/rezplugins/package_repository/memory.py +++ b/src/rezplugins/package_repository/memory.py @@ -11,14 +11,14 @@ from rez.package_resources import PackageFamilyResource, VariantResourceHelper, \ PackageResourceHelper, package_pod_schema from rez.utils.formatting import is_valid_package_name -from rez.utils.resources import ResourcePool, cached_property +from functools import cached_property +from rez.utils.resources import ResourcePool from rez.version import VersionedObject from typing import Any, Iterator, TYPE_CHECKING if TYPE_CHECKING: - from rez.packages import VariantResource - from rez.package_resources import PackageRepositoryResource + pass # This repository type is used when loading 'developer' packages (a package.yaml @@ -30,7 +30,8 @@ # resource classes #------------------------------------------------------------------------------ -class MemoryPackageFamilyResource(PackageFamilyResource): +class MemoryPackageFamilyResource( + PackageFamilyResource["MemoryPackageRepository", "MemoryPackageResource"]): key = "memory.family" repository_type = "memory" @@ -38,12 +39,12 @@ def _uri(self) -> str: return "%s:%s" % (self.location, self.name) def iter_packages(self) -> Iterator[MemoryPackageResource]: - data = self._repository.data.get(self.name, {}) + data = self.repository.data.get(self.name, {}) # check for unversioned package if "_NO_VERSION" in data: - package = self._repository.get_resource( - MemoryPackageResource.key, + package = self.repository.get_resource( + MemoryPackageResource, location=self.location, name=self.name) yield package @@ -51,15 +52,15 @@ def iter_packages(self) -> Iterator[MemoryPackageResource]: # versioned packages for version_str in data.keys(): - package = self._repository.get_resource( - MemoryPackageResource.key, + package = self.repository.get_resource( + MemoryPackageResource, location=self.location, name=self.name, version=version_str) yield package -class MemoryPackageResource(PackageResourceHelper): +class MemoryPackageResource(PackageResourceHelper["MemoryVariantResource"]): key = "memory.package" variant_key = "memory.variant" repository_type = "memory" @@ -74,15 +75,15 @@ def base(self) -> str | None: return None # memory types do not have 'base' @cached_property - def parent(self) -> PackageRepositoryResource: - family = self._repository.get_resource( - MemoryPackageFamilyResource.key, + def parent(self) -> MemoryPackageFamilyResource: + family = self.repository.get_resource( + MemoryPackageFamilyResource, location=self.location, name=self.name) return family def _load(self) -> dict[str, Any]: - family_data = self._repository.data.get(self.name, {}) + family_data = self.repository.data.get(self.name, {}) version_str = self.get("version") if not version_str: version_str = "_NO_VERSION" @@ -98,9 +99,9 @@ def _root(self, ignore_shortlinks: bool = False) -> str | None: return None # memory types do not have 'root' @cached_property - def parent(self) -> PackageRepositoryResource: - package = self._repository.get_resource( - MemoryPackageResource.key, + def parent(self) -> MemoryPackageResource: + package = self.repository.get_resource( + MemoryPackageResource, location=self.location, name=self.name, version=self.get("version") @@ -112,7 +113,8 @@ def parent(self) -> PackageRepositoryResource: # repository #------------------------------------------------------------------------------ -class MemoryPackageRepository(PackageRepository): +class MemoryPackageRepository( + PackageRepository[MemoryVariantResource, MemoryPackageResource, MemoryPackageFamilyResource]): """An in-memory package repository. Packages are stored in a dict, organised like so: @@ -179,7 +181,7 @@ def get_package_family(self, name: str) -> MemoryPackageFamilyResource | None: is_valid_package_name(name, raise_error=True) if name in self.data: family = self.get_resource( - MemoryPackageFamilyResource.key, + MemoryPackageFamilyResource, location=self.location, name=name) return family @@ -194,14 +196,14 @@ def iter_packages(self, package_family_resource: MemoryPackageFamilyResource) -> for package in package_family_resource.iter_packages(): yield package - def iter_variants(self, package_resource: PackageResourceHelper) -> Iterator[VariantResource]: + def iter_variants(self, package_resource: MemoryPackageResource) -> Iterator[MemoryVariantResource]: for variant in package_resource.iter_variants(): yield variant - def get_parent_package_family(self, package_resource: PackageResourceHelper) -> PackageFamilyResource: + def get_parent_package_family(self, package_resource: MemoryPackageResource) -> MemoryPackageFamilyResource: return package_resource.parent - def get_parent_package(self, variant_resource: VariantResource): + def get_parent_package(self, variant_resource: MemoryVariantResource) -> MemoryPackageResource: return variant_resource.parent