From 4834d9ec2c92a756d00295de50dafd40bac4078a Mon Sep 17 00:00:00 2001 From: Thibaud Gambier Date: Fri, 6 Dec 2024 22:26:32 +0100 Subject: [PATCH] initial commit --- .github/workflows/tests.yml | 68 +++ .gitignore | 13 + pyproject.toml | 126 ++++++ src/attribs/__init__.py | 67 +++ src/attribs/attributes.py | 535 ++++++++++++++++++++++++ src/attribs/base.py | 478 +++++++++++++++++++++ src/attribs/create.py | 129 ++++++ src/attribs/py.typed | 0 tests/__init__.py | 0 tests/conftest.py | 76 ++++ tests/test_attribute.py | 30 ++ tests/test_attributes.py | 218 ++++++++++ tests/test_matrix_property.py | 61 +++ tests/test_numeric_compound_property.py | 37 ++ tests/test_numeric_property.py | 67 +++ uv.lock | 297 +++++++++++++ 16 files changed, 2202 insertions(+) create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 pyproject.toml create mode 100644 src/attribs/__init__.py create mode 100644 src/attribs/attributes.py create mode 100644 src/attribs/base.py create mode 100644 src/attribs/create.py create mode 100644 src/attribs/py.typed create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_attribute.py create mode 100644 tests/test_attributes.py create mode 100644 tests/test_matrix_property.py create mode 100644 tests/test_numeric_compound_property.py create mode 100644 tests/test_numeric_property.py create mode 100644 uv.lock diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..09fcd9a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,68 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +# Cancel concurent in-progress jobs or run on pull_request +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + ruff-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v1 + with: + args: "check --output-format concise" + + ruff-format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v1 + with: + args: "format --diff" + + mypy: + name: Mypy ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12"] + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.pyton-version }} + - name: Run mypy + # Installing mypy here to bypass uv lockfile and run with a higher version + run: uv run --no-dev --group mypy --with mypy -- mypy + + tests: + name: Tests Maya ${{ matrix.maya-version }} + strategy: + matrix: + maya-version: ["2025", "2024", "2023", "2022"] + runs-on: ubuntu-latest + container: + image: tahv/mayapy:${{ matrix.maya-version }} + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v4 + - name: Sync environment + run: uv sync --no-dev --group test --group cov + - run: echo "PYTHONPATH=$(find .venv/lib/python*/site-packages -maxdepth 0):src" >> $GITHUB_ENV + - name: Run tests + run: uv run --no-sync -- mayapy -m coverage run -p -m pytest -vvv + - name: Upload coverage data + uses: actions/upload-artifact@v4 + with: + name: coverage-data-${{ matrix.maya-version }} + path: .coverage.* + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2a6ca53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +# Test Cov +.coverage diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2bbb567 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,126 @@ +#:schema https://json.schemastore.org/pyproject.json +[project] +name = "attribs" +description = "A Python library for creating Maya Attributes" +readme = "README.md" +license = { file = "LICENSE" } +requires-python = ">=3.7" +keywords = ["maya", "openmaya"] +authors = [{ name = "Thibaud Gambier" }] +dynamic = ["version"] +classifiers = [ + "Development Status :: 4 - Beta", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", +] +dependencies = ["typing-extensions ; python_version < '3.11'"] + +[dependency-groups] +dev = [ + { include-group = "test" }, + { include-group = "cov" }, + { include-group = "mypy" }, +] +# mypy = [ +# "mypy>=1.4.1", +# "mypy>=1.12.0; python_version > '3.7'", +# "maya-stubs>=0.4.1", +# { include-group = "test" } +# ] +mypy = ["maya-stubs>=0.4.1", { include-group = "test" }] +test = ["pytest>=7.4.4"] +cov = ["coverage>=7.2.7"] + +[build-system] +requires = ["setuptools", "setuptools-scm"] +build-backend = "setuptools.build_meta" + +[tool.setuptools_scm] +local_scheme = "no-local-version" + +[tool.pyright] +include = ["src"] +reportMissingModuleSource = false # Can't resolve maya modules sources + +[tool.ruff] +src = ["src"] +include = ["src/**/*.py", "tests/**/*.py", "**/pyproject.toml"] + +[tool.ruff.lint.isort] +required-imports = ["from __future__ import annotations"] + +[tool.ruff.lint] +select = ["ALL"] +pydocstyle.convention = "google" +flake8-tidy-imports.ban-relative-imports = "all" +ignore = [ + "D100", # Missing docstring in public module + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D107", # Missing docstring in `__init__` + "FIX002", # Line contains TODO, consider resolving the issue + "TD002", # Missing author in TODO + "TD003", # Missing issue link on the line following this TODO +] +unfixable = [ + "ERA001", # Found commented-out code + "F401", # Unused import +] + +[tool.ruff.lint.per-file-ignores] +"tests/**/test_*.py" = [ + "D103", # Missing docstring in public function + "INP001", # File is part of an implicit namespace package. Add an `__init__.py` + "PLR0913", # Too many arguments in function definition + "PLR2004", # Magic value used in comparison, consider replacing with a constant variable + "PT004", # Fixture does not return anything, add leading underscore + "PT011", # `pytest.raises(...)` is too broad + "S101", # Use of assert detected + +] +"tests/**/conftest.py" = [ + "INP001", # File is part of an implicit namespace package. Add an `__init__.py` +] + +[tool.mypy] +files = ["src", "tests"] +disallow_untyped_defs = true +check_untyped_defs = true +disallow_any_unimported = true +no_implicit_optional = true +warn_return_any = true +warn_unused_ignores = true +warn_redundant_casts = true +show_error_codes = true +# disallow_any_generics = true +# implicit_reexport = false +# enable_incomplete_feature = ["Unpack"] + +[[tool.mypy.overrides]] +# The `maya-stubs` package is incomplete +module = "attribs.attributes" +disable_error_code = ["attr-defined"] + +[tool.coverage.run] +source = ["src/"] +branch = true + +[tool.coverage.report] +show_missing = true +skip_covered = true +exclude_lines = [ + "# pragma: no cover", + "if (False|0|TYPE_CHECKING):", + "if __name__ == ['\"]__main__['\"]:", + # https://github.com/nedbat/coveragepy/issues/970 + "@overload", +] + +[tool.coverage.paths] +source = ["src/", "*/src"] diff --git a/src/attribs/__init__.py b/src/attribs/__init__.py new file mode 100644 index 0000000..32f8730 --- /dev/null +++ b/src/attribs/__init__.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from attribs.attributes import ( + Angle, + Angle2, + Angle3, + Angle4, + Bool, + Color, + Compound, + Curve, + Distance, + Distance2, + Distance3, + Double, + Double2, + Double3, + Double4, + DoubleMatrix, + Enum, + Float, + Float2, + Float3, + FloatMatrix, + Long, + Long2, + Long3, + Message, + Short, + Short2, + Short3, + String, +) +from attribs.create import add_attribute + +__all__ = [ + "Angle", + "Angle2", + "Angle3", + "Angle4", + "Bool", + "Color", + "Compound", + "Curve", + "Distance", + "Distance2", + "Distance3", + "Double", + "Double2", + "Double3", + "Double4", + "DoubleMatrix", + "Enum", + "Float", + "Float2", + "Float3", + "FloatMatrix", + "Long", + "Long2", + "Long3", + "Message", + "Short", + "Short2", + "Short3", + "String", + "add_attribute", +] diff --git a/src/attribs/attributes.py b/src/attribs/attributes.py new file mode 100644 index 0000000..532fe7f --- /dev/null +++ b/src/attribs/attributes.py @@ -0,0 +1,535 @@ +from __future__ import annotations + +import sys +from types import MappingProxyType +from typing import Sequence, Union, cast + +from maya.api import OpenMaya + +from attribs.base import ( + Attribute, + AttributeKwargs, + MatrixProperty, + NumericAttribute, + NumericCompoundAttribute, +) + +if sys.version_info < (3, 11): + from typing_extensions import Unpack +else: + from typing import Unpack + + +class Message(Attribute): + """Message attribute. + + Used to declare relationships between nodes. + + Doesn't contain or transmit any data. + Its only purpose is to declare relationships between nodes + by connecting 2 messages plugs together. + """ + + MFN_CLS = OpenMaya.MFnMessageAttribute + + +class DoubleMatrix(Attribute): + """64-bit double precision ``OpenMaya.MMatrix`` attribute.""" + + MFN_CLS = OpenMaya.MFnMatrixAttribute + DATA_TYPE = cast(int, OpenMaya.MFnMatrixAttribute.kDouble) + + default = MatrixProperty() + + def __init__( + self, + name: str, + *, + default: Sequence[float] + | OpenMaya.MMatrix + | OpenMaya.MTransformationMatrix + | None = None, + **kwargs: Unpack[AttributeKwargs], + ) -> None: + super().__init__(name, **kwargs) + setattr(self, "default", default) # noqa: B010 + + +class FloatMatrix(Attribute): + """32-bit single precision ``OpenMaya.MMatrix`` attribute.""" + + MFN_CLS = OpenMaya.MFnMatrixAttribute + DATA_TYPE = cast(int, OpenMaya.MFnMatrixAttribute.kFloat) + + default = MatrixProperty() + + def __init__( + self, + name: str, + *, + default: Sequence[float] + | OpenMaya.MMatrix + | OpenMaya.MTransformationMatrix + | None = None, + **kwargs: Unpack[AttributeKwargs], + ) -> None: + super().__init__(name, **kwargs) + setattr(self, "default", default) # noqa: B010 + + +class String(Attribute): + """String attribute.""" + + MFN_CLS = OpenMaya.MFnTypedAttribute + DATA_TYPE = OpenMaya.MFnData.kString + + def __init__( + self, + name: str, + *, + default: str = "", + as_filename: bool = False, + **kwargs: Unpack[AttributeKwargs], + ) -> None: + super().__init__(name, **kwargs) + self.as_filename: bool = as_filename + self.default: str = default + + +class Curve(Attribute): + """Curve attribute.""" + + MFN_CLS = OpenMaya.MFnTypedAttribute + DATA_TYPE = OpenMaya.MFnData.kNurbsCurve + + +class Bool(Attribute): + """Boolean attribute.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.kBoolean) + + def __init__( + self, + name: str, + *, + default: bool = False, + **kwargs: Unpack[AttributeKwargs], + ) -> None: + super().__init__(name, **kwargs) + self.default: bool = default + + +class Compound(Attribute): + """Container of attributes. + + A Compound attribute allow the grouping of related attributes into a larger unit. + """ + + MFN_CLS = OpenMaya.MFnCompoundAttribute + + def __init__( + self, + name: str, + *, + children: Sequence[Attribute] = (), + **kwargs: Unpack[AttributeKwargs], + ) -> None: + super().__init__(name, **kwargs) + self._children: list[Attribute] = list(children) + + def append(self, attribute: Attribute) -> None: # noqa: D102 + self._children.append(attribute) + + @property + def children(self) -> tuple[Attribute, ...]: # noqa: D102 + return tuple(self._children) + + +class Enum(Attribute): + """Enum attribute.""" + + MFN_CLS = OpenMaya.MFnEnumAttribute + + def __init__( + self, + name: str, + *, + fields: dict[int, str] | list[str] | None = None, + default: int | None = None, + **kwargs: Unpack[AttributeKwargs], + ) -> None: + super().__init__(name, **kwargs) + self._fields: dict[int, str] = {} + + if isinstance(fields, list): + for index, field in enumerate(fields): + self._fields[index] = field + + elif isinstance(fields, dict): + for index, field in fields.items(): + self._fields[index] = field + + self._default: int | None = None + if default is not None: + self.default = default + + @property + def fields(self) -> MappingProxyType[int, str]: # noqa: D102 + return MappingProxyType(self._fields) + + @property + def default(self) -> int: + """Default index.""" + if self._default is not None: + return self._default + if self._fields: + return min(self._fields.keys()) + return 0 + + @default.setter + def default(self, value: int) -> None: + if value not in self._fields: + message = f"Invalid index `{value}`" + raise ValueError(message) + self._default = value + + +class Double(NumericAttribute[float, None]): + """64-bit double precision attribute.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.kDouble) + NUMERIC_CLS = float + DEFAULT_UNIT = None + + +class Double2( + NumericCompoundAttribute[ + tuple[Double, Double], + tuple[float, float], + tuple[Union[float, None], Union[float, None]], + None, + ], +): + """Numeric compound of 2 `Double` attributes.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.k2Double) + CHILD_CLS = Double + CHILD_SUFFIXES = ("X", "Y") + DEFAULT_UNIT = None + + +class Double3( + NumericCompoundAttribute[ + tuple[Double, Double, Double], + tuple[float, float, float], + tuple[Union[float, None], Union[float, None], Union[float, None]], + None, + ], +): + """Numeric compound of 3 `Double` attributes.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.k3Double) + CHILD_CLS = Double + CHILD_SUFFIXES = ("X", "Y", "Z") + DEFAULT_UNIT = None + + +class Double4( + NumericCompoundAttribute[ + tuple[Double, Double, Double, Double], + tuple[float, float, float, float], + tuple[ + Union[float, None], + Union[float, None], + Union[float, None], + Union[float, None], + ], + None, + ], +): + """Numeric compound of 4 `Double` attributes.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.k4Double) + CHILD_CLS = Double + CHILD_SUFFIXES = ("X", "Y", "Z", "W") + DEFAULT_UNIT = None + + +# TODO: Scale ? +# TODO: Quaternion ? + + +class Distance(NumericAttribute[OpenMaya.MDistance, int]): + """`OpenMaya.MDistance` attribute.""" + + MFN_CLS = OpenMaya.MFnUnitAttribute + DATA_TYPE = cast(int, OpenMaya.MFnUnitAttribute.kDistance) + NUMERIC_CLS = OpenMaya.MDistance + DEFAULT_UNIT = OpenMaya.MDistance.kCentimeters + + +class Distance2( + NumericCompoundAttribute[ + tuple[Distance, Distance], + tuple[OpenMaya.MDistance, OpenMaya.MDistance], + tuple[ + Union[float, None, OpenMaya.MDistance], + Union[float, None, OpenMaya.MDistance], + ], + int, + ], +): + """Numeric compound of 2 `Distance` attributes.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.k2Double) + CHILD_CLS = Distance + CHILD_SUFFIXES = ("X", "Y") + DEFAULT_UNIT = OpenMaya.MDistance.kCentimeters + + +class Distance3( + NumericCompoundAttribute[ + tuple[Distance, Distance, Distance], + tuple[OpenMaya.MDistance, OpenMaya.MDistance, OpenMaya.MDistance], + tuple[ + Union[float, None, OpenMaya.MDistance], + Union[float, None, OpenMaya.MDistance], + Union[float, None, OpenMaya.MDistance], + ], + int, + ], +): + """Numeric compound of 3 `Distance` attributes.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.k3Double) + CHILD_CLS = Distance + CHILD_SUFFIXES = ("X", "Y", "Z") + DEFAULT_UNIT = OpenMaya.MDistance.kCentimeters + + +class Angle(NumericAttribute[OpenMaya.MAngle, int]): + """`OpenMaya.MAngle` attribute.""" + + MFN_CLS = OpenMaya.MFnUnitAttribute + DATA_TYPE = cast(int, OpenMaya.MFnUnitAttribute.kAngle) + NUMERIC_CLS = OpenMaya.MAngle + DEFAULT_UNIT = OpenMaya.MAngle.kDegrees + + +class Angle2( + NumericCompoundAttribute[ + tuple[Angle, Angle], + tuple[OpenMaya.MAngle, OpenMaya.MAngle], + tuple[ + Union[float, None, OpenMaya.MAngle], + Union[float, None, OpenMaya.MAngle], + ], + int, + ], +): + """Numeric compound of 2 `Angle` attributes.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.k2Double) + CHILD_CLS = Angle + CHILD_SUFFIXES = ("X", "Y") + DEFAULT_UNIT = OpenMaya.MAngle.kDegrees + + +class Angle3( + NumericCompoundAttribute[ + tuple[Angle, Angle, Angle], + tuple[OpenMaya.MAngle, OpenMaya.MAngle, OpenMaya.MAngle], + tuple[ + Union[float, None, OpenMaya.MAngle], + Union[float, None, OpenMaya.MAngle], + Union[float, None, OpenMaya.MAngle], + ], + int, + ], +): + """Numeric compound of 3 `Angle` attributes.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.k3Double) + CHILD_CLS = Angle + CHILD_SUFFIXES = ("X", "Y", "Z") + DEFAULT_UNIT = OpenMaya.MAngle.kDegrees + + +class Angle4( + NumericCompoundAttribute[ + tuple[Angle, Angle, Angle, Angle], + tuple[OpenMaya.MAngle, OpenMaya.MAngle, OpenMaya.MAngle, OpenMaya.MAngle], + tuple[ + Union[float, None, OpenMaya.MAngle], + Union[float, None, OpenMaya.MAngle], + Union[float, None, OpenMaya.MAngle], + Union[float, None, OpenMaya.MAngle], + ], + int, + ], +): + """Numeric compound of 4 `Angle` attributes.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.k4Double) + CHILD_CLS = Angle + CHILD_SUFFIXES = ("X", "Y", "Z", "W") + DEFAULT_UNIT = OpenMaya.MAngle.kDegrees + + +class Float(NumericAttribute[float, None]): + """32-bit single precision attribute.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.kFloat) + NUMERIC_CLS = float + DEFAULT_UNIT = None + + +class Float2( + NumericCompoundAttribute[ + tuple[Float, Float], + tuple[float, float], + tuple[Union[float, None], Union[float, None]], + None, + ], +): + """Numeric compound of 2 `Float` attributes.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.k2Float) + CHILD_CLS = Float + CHILD_SUFFIXES = ("X", "Y") + DEFAULT_UNIT = None + + +class Float3( + NumericCompoundAttribute[ + tuple[Float, Float, Float], + tuple[float, float, float], + tuple[Union[float, None], Union[float, None], Union[float, None]], + None, + ], +): + """Numeric compound of 3 `Float` attributes.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.k3Float) + CHILD_CLS = Float + CHILD_SUFFIXES = ("X", "Y", "Z") + DEFAULT_UNIT = None + + +class Color( + NumericCompoundAttribute[ + tuple[Float, Float, Float], + tuple[float, float, float], + tuple[Union[float, None], Union[float, None], Union[float, None]], + None, + ], +): + """Numeric compound of 3 `Float` attributes with suffixes ``R, G, B``. + + Displayed with a color picker in the Attribute Editor. + """ + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.k3Float) + CHILD_CLS = Float + CHILD_SUFFIXES = ("R", "G", "B") + DEFAULT_UNIT = None + + +class Long(NumericAttribute[int, None]): + """32-bit integer attribute.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.kLong) + NUMERIC_CLS = int + DEFAULT_UNIT = None + + +class Long2( + NumericCompoundAttribute[ + tuple[Long, Long], + tuple[int, int], + tuple[Union[int, None], Union[int, None]], + None, + ], +): + """Numeric compound of 2 `Long` attributes.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.k2Long) + CHILD_CLS = Long + CHILD_SUFFIXES = ("X", "Y") + DEFAULT_UNIT = None + + +class Long3( + NumericCompoundAttribute[ + tuple[Long, Long, Long], + tuple[int, int, int], + tuple[Union[int, None], Union[int, None], Union[int, None]], + None, + ], +): + """Numeric compound of 3 `Long` attributes.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.k3Long) + CHILD_CLS = Long + CHILD_SUFFIXES = ("X", "Y", "Z") + DEFAULT_UNIT = None + + +class Short(NumericAttribute[int, None]): + """16-bit integer attribute.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.kShort) + NUMERIC_CLS = int + DEFAULT_UNIT = None + + +class Short2( + NumericCompoundAttribute[ + tuple[Short, Short], + tuple[int, int], + tuple[Union[int, None], Union[int, None]], + None, + ], +): + """Numeric compound of 2 `Short` attributes.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.k2Short) + CHILD_CLS = Short + CHILD_SUFFIXES = ("X", "Y") + DEFAULT_UNIT = None + + +class Short3( + NumericCompoundAttribute[ + tuple[Short, Short, Short], + tuple[int, int, int], + tuple[Union[int, None], Union[int, None], Union[int, None]], + None, + ], +): + """Numeric compound of 3 `Short` attributes.""" + + MFN_CLS = OpenMaya.MFnNumericAttribute + DATA_TYPE = cast(int, OpenMaya.MFnNumericData.k3Short) + CHILD_CLS = Short + CHILD_SUFFIXES = ("X", "Y", "Z") + DEFAULT_UNIT = None diff --git a/src/attribs/base.py b/src/attribs/base.py new file mode 100644 index 0000000..87b299c --- /dev/null +++ b/src/attribs/base.py @@ -0,0 +1,478 @@ +from __future__ import annotations + +import sys +from typing import ( + ClassVar, + Generic, + Literal, + Sequence, + TypedDict, + TypeVar, + Union, + cast, + overload, +) + +from maya.api import OpenMaya + +if sys.version_info < (3, 11): + from typing_extensions import Unpack +else: + from typing import Unpack + +MayaNumeric = Union[float, OpenMaya.MAngle, OpenMaya.MDistance, OpenMaya.MTime] +NT = TypeVar("NT", bound=MayaNumeric) +UT = TypeVar("UT", bound=Union[int, None]) + + +NCA = TypeVar( + "NCA", + bound=( + Union[ + tuple[ + "NumericAttribute", + "NumericAttribute", + ], + tuple[ + "NumericAttribute", + "NumericAttribute", + "NumericAttribute", + ], + tuple[ + "NumericAttribute", + "NumericAttribute", + "NumericAttribute", + "NumericAttribute", + ], + ] + ), +) +"""Children type of NumericCompound.""" + +NCGT = TypeVar( + "NCGT", + bound=( + Union[ + tuple[ + Union[MayaNumeric, None], + Union[MayaNumeric, None], + ], + tuple[ + Union[MayaNumeric, None], + Union[MayaNumeric, None], + Union[MayaNumeric, None], + ], + tuple[ + Union[MayaNumeric, None], + Union[MayaNumeric, None], + Union[MayaNumeric, None], + Union[MayaNumeric, None], + ], + ] + ), +) +"""NumericCompound getter type.""" + +NCST = TypeVar( + "NCST", + bound=( + Union[ + tuple[ + Union[MayaNumeric, float, None], + Union[MayaNumeric, float, None], + ], + tuple[ + Union[MayaNumeric, float, None], + Union[MayaNumeric, float, None], + Union[MayaNumeric, float, None], + ], + tuple[ + Union[MayaNumeric, float, None], + Union[MayaNumeric, float, None], + Union[MayaNumeric, float, None], + Union[MayaNumeric, float, None], + ], + ] + ), +) +"""NumericCompound setter Type.""" + +GT = TypeVar("GT") +"""Generic getter type for NumericCompoundProperty.""" + +ST = TypeVar("ST") +"""Generic setter type for NumericCompoundProperty.""" + + +class AttributeKwargs(TypedDict, total=False): + """`Attribute` keyword arguments.""" + + affects_appearence: bool + affects_world_space: bool + # TODO: array: bool = False + cached: bool + short_name: str + channel_box: bool + connectable: bool + # TODO: dynamic (read only property) + # TODO: extension (read only property) + disconnect_behavior: Literal[0, 1, 2] + hidden: bool + indeterminant: bool + index_matters: bool + internal: bool # TODO: more doc + # TODO: is_proxy_attribute + keyable: bool + # TODO: self.parent = parent + readable: bool + render_source: bool # TODO: more doc + storable: bool + # TODO: used_as_color: in Color attribute + # TODO: uses_array_data_builder: bool = False (in Array attr) + # TODO: world_space - on array ? + writable: bool + + +class Attribute: + """Attributes base class. + + Child class must implement `MFN_CLS` and `DATA_TYPE`. + """ + + MFN_CLS: ClassVar[type[OpenMaya.MFnAttribute]] = OpenMaya.MFnAttribute + DATA_TYPE: ClassVar[int] = OpenMaya.MFnData.kInvalid + + def __init__(self, name: str, /, **kwargs: Unpack[AttributeKwargs]) -> None: + self.name = name + + self.short_name: str | None = kwargs.pop("short_name", None) + """Attribute short name. + + If the attribute has no short name then its long name will be used. + """ + + self.affects_appearence: bool = kwargs.pop("affects_appearence", False) + """Attribute affects appearence of object when rendered in viewport.""" + + self.affects_world_space: bool = kwargs.pop("affects_world_space", False) + """Attribute affects the node world space matrix. DAG nodes only.""" + + # TODO: array: bool = False + + self.cached: bool = kwargs.pop("cached", True) + """Should this attribute value be cached locally in the node data block. + + Caching a node locally causes a copy of the attribute value + for the node to be cached with the node. + This removes the need to traverse through the graph + to get the value each time it is requested. + + Caching give a speed increase at the cost of more memory. + """ + + self.channel_box: bool = kwargs.pop("channel_box", False) + """Should the attribute appear in the channel box when the node is selected. + + Attributes will appear in the channel box if their `channel_box` flag is set + or if they are `keyable`. + """ + + self.connectable: bool = kwargs.pop("connectable", True) + """Should this attribute allow dependency graph connections. + + If the attribute is `connectable`, the `readable` and `writable` properties + will indicate what types of connections are accepted. + """ + + self.disconnect_behavior: Literal[0, 1, 2] = kwargs.pop( + "disconnect_behavior", + OpenMaya.MFnAttribute.kNothing, # type: ignore[typeddict-item] + ) # type: ignore[assignment] + """Behavior when this attribute gets disconnected. + + Possible values: + - ``OpenMaya.MFnAttribute.kDelete`` (``0``): + Delete array element (`array` attributes only). + - ``OpenMaya.MFnAttribute.kReset`` (``1``): + Reset the attribute to its default value. + - ``OpenMaya.MFnAttribute.kNothing`` (``2``): + Do nothing to the attribute value. + """ + + self.hidden: bool = kwargs.pop("hidden", False) + """Should this attribute not be displayed in the Attribute Editor.""" + + self.indeterminant: bool = kwargs.pop("indeterminant", False) + """Whether the attribute may be used during evaluation. + + This attribute may not always be used when computing the attributes which + are dependent upon it. + + This property is mainly used on rendering nodes to indicate that some + attribute are not always used. + """ + + self.index_matters: bool = kwargs.pop("index_matters", True) + """Whether the user must specify an index when connecting to this attribute. + + - If `False`, ``connectAttr -nextAvailable`` can be used with this attribute. + - If `True`, then an explicit index must be provided. + + If the destination attribute has set the indexMatters to be False with + this flag specified, a connection is made to the next available index, + no index need be specified. + + This only affect array attributes which are not `readable`, + like destination attributes. + """ + + self.internal: bool = kwargs.pop("internal", False) + """Will the node handle the attribute data storage itself, + outside of the node data block. + """ + + # TODO: is_proxy_attribute + + self.keyable: bool = kwargs.pop("keyable", False) + """Keys can be set on the attribute.""" + + # TODO: self.parent = parent + + self.readable: bool = kwargs.pop("readable", True) + """Attribute can be used as the source in a dependency graph connection.""" + + self.render_source: bool = kwargs.pop("render_source", False) + """Attribute is used on rendering nodes to override rendering sampler info.""" + + self.storable: bool = kwargs.pop("storable", True) + """Should the attribute value be stored when the node is written to file.""" + + # TODO: used_as_color: in Color attribute + # TODO: used_as_filename: in FIleName attribute + # TODO: uses_array_data_builder: bool = False (in Array attr) + # TODO: world_space - on array ? + + self.writable: bool = kwargs.pop("writable", True) + """Is the attribute writable. + + If an attribute is writable, then it can be used as the destination in + a dependency graph connection. + """ + + +class MatrixProperty: + """Matrix property.""" + + def __init__(self) -> None: + self._default: OpenMaya.MMatrix = OpenMaya.MMatrix.kIdentity + + def __set_name__(self, owner: type[object], name: str) -> None: + self.name = name + + @overload + def __get__(self, obj: None, objtype: None) -> None: ... + + @overload + def __get__(self, obj: object, objtype: type[object]) -> OpenMaya.MMatrix: ... + + def __get__( + self, + obj: object | None, + objtype: type[object] | None = None, + ) -> OpenMaya.MMatrix | None: + return obj.__dict__.get(self.name, self._default) # type: ignore[no-any-return] + + def __set__( + self, + obj: object, + value: Sequence[float] | OpenMaya.MMatrix | OpenMaya.MTransformationMatrix, + ) -> None: + if isinstance(value, Sequence): + try: + value = OpenMaya.MMatrix(value) + except ValueError: + if len(value) != 16: # noqa: PLR2004 + message = f"Expected 16 floats, got {len(value)}" + raise ValueError(message) from None + raise + elif isinstance(value, OpenMaya.MTransformationMatrix): + value = value.asMatrix() + if not isinstance(value, OpenMaya.MMatrix): + raise TypeError(type(value)) + obj.__dict__[self.name] = value + + +class NumericProperty(Generic[NT]): + """`NumericAttribute` property.""" + + def __set_name__(self, owner: type[object], name: str) -> None: + self.name = name + + @overload + def __get__(self, obj: None, objtype: None) -> None: ... + + @overload + def __get__(self, obj: object, objtype: type[object]) -> NT | None: ... + + def __get__( + self, + obj: object | None, + objtype: type[object] | None = None, + ) -> NT | None: + return obj.__dict__.get(self.name) + + def __set__(self, obj: object, value: float | NT | None) -> None: + if value is None: + obj.__dict__[self.name] = None + return + + numeric_cls = cast("type[NT]", getattr(obj, "NUMERIC_CLS", float)) + unit = cast("int | None", getattr(obj, "unit", None)) + + if issubclass(numeric_cls, (float, int)): + obj.__dict__[self.name] = numeric_cls(value) # type: ignore[arg-type] + return + + if not isinstance(value, numeric_cls): + args = [unit] if unit is not None else [] + converted = numeric_cls(value, *args) + elif unit is not None and value.unit != unit: + converted = numeric_cls(value.asUnits(unit), unit) + else: + converted = value + + obj.__dict__[self.name] = converted + + +class NumericAttribute(Attribute, Generic[NT, UT]): + """Base class for Maya Numeric Attributes. + + Child class must implement `MFN_CLS`, `DATA_TYPE`, `NUMERIC_CLS` and `DEFAULT_UNIT`. + """ + + NUMERIC_CLS: ClassVar[type[MayaNumeric]] + DEFAULT_UNIT: ClassVar[int | None] + + default = NumericProperty[NT]() + min = NumericProperty[NT]() + max = NumericProperty[NT]() + soft_min = NumericProperty[NT]() + soft_max = NumericProperty[NT]() + + def __init__( # noqa: PLR0913 + self, + name: str, + /, + *, + default: NT | float | None = None, + min: NT | float | None = None, # noqa: A002 + max: NT | float | None = None, # noqa: A002 + soft_min: NT | float | None = None, + soft_max: NT | float | None = None, + unit: UT | None = None, + **kwargs: Unpack[AttributeKwargs], + ) -> None: + super().__init__(name, **kwargs) + self.unit: UT = unit if unit is not None else self.DEFAULT_UNIT # type: ignore[assignment] + setattr(self, "default", default) # noqa: B010 + setattr(self, "min", min) # noqa: B010 + setattr(self, "max", max) # noqa: B010 + setattr(self, "soft_min", soft_min) # noqa: B010 + setattr(self, "soft_max", soft_max) # noqa: B010 + + +class NumericCompoundProperty(Generic[GT, ST]): + """Attribute numeric compound property.""" + + def __init__(self, child_attr: str | None = None) -> None: + self._child_attr: str | None = child_attr + + def __set_name__(self, owner: type[object], name: str) -> None: + self.name = name + + @property + def child_attr(self) -> str: # noqa: D102 + return self._child_attr or self.name + + def get_children(self, obj: object) -> tuple[NumericAttribute, ...]: # noqa: D102 + return getattr(obj, "children", ()) + + @overload + def __get__(self, obj: None, objtype: None) -> None: ... + + @overload + def __get__(self, obj: object, objtype: type[object]) -> GT: ... + + def __get__( + self, + obj: object | None, + objtype: type[object] | None = None, + ) -> GT | None: + result = [] + for c in self.get_children(obj): + value = getattr(c, self.child_attr, None) + result.append(value) + return tuple(result) # type: ignore[return-value] + + def __set__(self, obj: object, value: ST) -> None: + backup: list[tuple[object, object]] = [] + + children = self.get_children(obj) + + if value is None: + value = tuple(None for _ in children) + + for child, val in zip(children, value): # type: ignore[call-overload] + bckp_val = getattr(child, self.child_attr) + try: + setattr(child, self.child_attr, val) + except Exception: + for backup_child, backup_value in backup: + setattr(backup_child, self.child_attr, backup_value) + raise + else: + backup.append((child, bckp_val)) + + +class NumericCompoundAttribute(Attribute, Generic[NCA, NCGT, NCST, UT]): + """Base class for Maya Numeric Compound Attributes. + + Child class must implement `MFN_CLS`, `DATA_TYPE`, `CHILD_CLS`, `CHILD_SUFFIXES` + and `DEFAULT_UNIT`. + """ + + CHILD_CLS: ClassVar[type[NumericAttribute]] + CHILD_SUFFIXES: ClassVar[tuple[str, ...]] + DEFAULT_UNIT: ClassVar[int | None] + + default = NumericCompoundProperty[NCGT, NCST]() + min = NumericCompoundProperty[NCGT, NCST]() + max = NumericCompoundProperty[NCGT, NCST]() + soft_min = NumericCompoundProperty[NCGT, NCST]() + soft_max = NumericCompoundProperty[NCGT, NCST]() + + def __init__( # noqa: PLR0913 + self, + name: str, + /, + default: NCST | None = None, + min: NCST | None = None, # noqa: A002 + max: NCST | None = None, # noqa: A002 + soft_min: NCST | None = None, + soft_max: NCST | None = None, + unit: UT | None = None, + **kwargs: Unpack[AttributeKwargs], + ) -> None: + super().__init__(name, **kwargs) + + children: list[NumericAttribute] = [] + for s in self.CHILD_SUFFIXES: + children.append(self.CHILD_CLS(f"{name}{s}")) # noqa: PERF401 + self.children: NCA = tuple(children) # type: ignore[assignment] + + self.unit: UT = unit if unit is not None else self.DEFAULT_UNIT # type: ignore[assignment] + setattr(self, "default", default) # noqa: B010 + setattr(self, "min", min) # noqa: B010 + setattr(self, "max", max) # noqa: B010 + setattr(self, "soft_min", soft_min) # noqa: B010 + setattr(self, "soft_max", soft_max) # noqa: B010 diff --git a/src/attribs/create.py b/src/attribs/create.py new file mode 100644 index 0000000..35d2e86 --- /dev/null +++ b/src/attribs/create.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +from typing import cast + +from maya.api import OpenMaya + +from attribs.attributes import ( + Bool, + Color, + Compound, + DoubleMatrix, + Enum, + FloatMatrix, + String, +) +from attribs.base import Attribute, NumericAttribute, NumericCompoundAttribute + + +class LockedNodeError(Exception): + """Raised when an operation is requested on a locked node.""" + + +def add_attribute( + node: OpenMaya.MFnDependencyNode, + attribute: Attribute, + modifier: OpenMaya.MDGModifier, +) -> OpenMaya.MPlug: + """Add ``attribute`` to ``node`` and returns it as a `OpenMaya.MPlug`.""" + if node.isLocked: + raise LockedNodeError(node.uniqueName()) + + modifier.addAttribute(node.object(), create_mobject(attribute)) + modifier.doIt() + + # TODO: Attribute.lock property + # stack = [attribute] + # while stack: + # current = stack.pop() + # plug = cast(OpenMaya.MPlug, node.findPlug(current.name, False)) + # plug.isLocked = current.locked + # stack.extend(getattr(current, "children", [])) + + plug = cast(OpenMaya.MPlug, node.findPlug(attribute.name, False)) # noqa: FBT003 + + if isinstance(attribute, (DoubleMatrix, FloatMatrix)): + value = OpenMaya.MFnMatrixData().create(attribute.default) + modifier.newPlugValue(plug, value) + modifier.doIt() + + return plug + + +def create_mobject(attribute: Attribute) -> OpenMaya.MObject: # noqa: C901 + """Create a mobject from ``attribute``.""" + mfn = attribute.MFN_CLS() + + mobject: OpenMaya.MObject = mfn.create(*get_mfn_args(attribute)) # type: ignore[attr-defined] + + mfn.keyable = attribute.keyable + mfn.channelBox = attribute.channel_box + mfn.hidden = attribute.hidden + mfn.storable = attribute.storable + mfn.connectable = attribute.connectable + mfn.readable = attribute.readable + mfn.writable = attribute.writable + # mfn.array = attribute.array + mfn.disconnectBehavior = attribute.disconnect_behavior + + if isinstance(attribute, NumericAttribute): + mfn = cast(OpenMaya.MFnNumericAttribute, mfn) + if attribute.min is not None: + mfn.setMin(attribute.min) + if attribute.max is not None: + mfn.setMax(attribute.max) + if attribute.soft_min is not None: + mfn.setSoftMin(attribute.soft_min) + if attribute.soft_max is not None: + mfn.setSoftMax(attribute.soft_max) + + if isinstance(attribute, Enum): + mfn = cast(OpenMaya.MFnEnumAttribute, mfn) + for index, field in attribute.fields.items(): + mfn.addField(field, index) + + elif isinstance(attribute, String): + mfn.usedAsFilename = attribute.as_filename + + elif isinstance(attribute, Color): + mfn.usedAsColor = True + + elif isinstance(attribute, Compound): + mfn = cast(OpenMaya.MFnCompoundAttribute, mfn) + for child in attribute.children: + mfn.addChild(create_mobject(child)) + + return mobject + + +def get_mfn_args(attribute: Attribute) -> list: + """Returns the list of arguments to pass to `OpenMaya.MFnAttibute.create`.""" + args: list = [attribute.name, attribute.short_name or attribute.name] + + if isinstance(attribute, NumericCompoundAttribute): + args.extend(create_mobject(c) for c in attribute.children) + return args + + if attribute.DATA_TYPE != OpenMaya.MFnData.kInvalid: # noqa: SIM300 + args.append(attribute.DATA_TYPE) + + if isinstance(attribute, (Bool, Enum)): + args.append(attribute.default) + + elif isinstance(attribute, String): + string = OpenMaya.MFnStringData() + default = cast(OpenMaya.MObject, string.create()) + string.set(attribute.default) + args.append(default) + + elif isinstance(attribute, NumericAttribute) and attribute.default is not None: + default = attribute.default + default = getattr(default, "asUnits", lambda _: default)(attribute.unit) + args.append(default) + + # NOTE: Matrix also have a `default` property, + # but `OpenMaya.MFnMatrixAttribute.create` + # doesn't accept a `default` argument. + # It must be set after the attribute is created. + + return args diff --git a/src/attribs/py.typed b/src/attribs/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f011e11 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,76 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Generator + +import pytest +from tests.test_attributes import cast + +if TYPE_CHECKING: + from maya.api import OpenMaya + + +@pytest.fixture(scope="session", autouse=True) +def initialize_maya_session() -> Generator[None, None, None]: + """Initialize Maya standalone.""" + import maya.standalone + + maya.standalone.initialize() + + yield + + maya.standalone.uninitialize() + + +@pytest.fixture(autouse=True) +def new_maya_scene() -> None: + """Create a new file and restore default FBX options before each test.""" + from maya import cmds + + cmds.file(new=True, force=True) # type: ignore[call-arg] + + +@pytest.fixture +def network_node() -> Generator[OpenMaya.MFnDependencyNode, None, None]: + """Create a `network` node for the duration of the test.""" + from maya.api import OpenMaya + + modifier = OpenMaya.MDGModifier() + node = _create_dependency_node("network", modifier) + + yield OpenMaya.MFnDependencyNode(node) + + modifier.deleteNode(node) + modifier.doIt() + + +def _create_dependency_node( + node_type: str, + modifier: OpenMaya.MDGModifier, + *, + name: str | None = None, +) -> OpenMaya.MObject: + from maya.api import OpenMaya + + node_mob = cast(OpenMaya.MObject, modifier.createNode(node_type)) + if name is not None: + modifier.renameNode(node_mob, name) + modifier.doIt() + return node_mob + + +def _create_dag_node( + node_type: str, + modifier: OpenMaya.MDagModifier, + *, + name: str | None = None, + parent: OpenMaya.MObject | None = None, +) -> OpenMaya.MObject: + from maya.api import OpenMaya + + args = [node_type, parent] if parent is not None else [node_type] + node_mob = cast(OpenMaya.MObject, modifier.createNode(*args)) + if name is not None: + modifier.renameNode(node_mob, name) + modifier.doIt() + # TODO: If a shape gets created, this return the shape, not the transform. + return node_mob diff --git a/tests/test_attribute.py b/tests/test_attribute.py new file mode 100644 index 0000000..05bfc4f --- /dev/null +++ b/tests/test_attribute.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import pytest +from maya.api import OpenMaya + +from attribs.base import Attribute + + +@pytest.mark.parametrize( + ("attr", "expected"), + [ + ("affects_appearence", False), + ("affects_world_space", False), + ("cached", True), + ("channel_box", False), + ("connectable", True), + ("disconnect_behavior", OpenMaya.MFnAttribute.kNothing), + ("hidden", False), + ("indeterminant", False), + ("index_matters", True), + ("internal", False), + ("keyable", False), + ("readable", True), + ("render_source", False), + ("storable", True), + ("writable", True), + ], +) +def test_attribute_defaults(attr: str, expected: object) -> None: + assert getattr(Attribute("foo"), attr) == expected diff --git a/tests/test_attributes.py b/tests/test_attributes.py new file mode 100644 index 0000000..c16095e --- /dev/null +++ b/tests/test_attributes.py @@ -0,0 +1,218 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, cast + +import pytest +from maya.api import OpenMaya + +from attribs.attributes import ( + Angle, + Angle2, + Angle3, + Angle4, + Bool, + Color, + Compound, + Curve, + Distance, + Distance2, + Distance3, + Double, + Double2, + Double3, + Double4, + DoubleMatrix, + Enum, + Float, + Float2, + Float3, + FloatMatrix, + Long, + Long2, + Long3, + Message, + Short, + Short2, + Short3, + String, +) +from attribs.create import add_attribute + +if TYPE_CHECKING: + from attribs.base import NumericAttribute, NumericCompoundAttribute + + +def test_create_message(network_node: OpenMaya.MFnDependencyNode) -> None: + attribute = Message("foo") + modifier = OpenMaya.MDGModifier() + plug = add_attribute(network_node, attribute, modifier) + + mattribute = cast(OpenMaya.MObject, plug.attribute()) + assert mattribute.apiType() == OpenMaya.MFn.kMessageAttribute + + +@pytest.mark.parametrize(("attribute_cls"), [FloatMatrix, DoubleMatrix]) +def test_create_matrix( + attribute_cls: type[FloatMatrix | DoubleMatrix], + network_node: OpenMaya.MFnDependencyNode, +) -> None: + default = OpenMaya.MMatrix([1, 2, 2, 0, 2, 1, 2, 0, 2, 2, 1, 0, 2, 2, 2, 1]) + attribute = attribute_cls("foo", default=default) + modifier = OpenMaya.MDGModifier() + plug = add_attribute(network_node, attribute, modifier) + + mattribute = cast(OpenMaya.MObject, plug.attribute()) + assert mattribute.apiType() == OpenMaya.MFn.kMatrixAttribute + + data = OpenMaya.MFnMatrixData(plug.asMObject()) + assert OpenMaya.MMatrix(data.matrix()).isEquivalent(default) + + +def test_create_string(network_node: OpenMaya.MFnDependencyNode) -> None: + attribute = String("foo", default="foobar", as_filename=True) + + modifier = OpenMaya.MDGModifier() + plug = add_attribute(network_node, attribute, modifier) + assert plug.asString() == "foobar" + + mobject = cast(OpenMaya.MObject, plug.attribute()) + assert mobject.apiType() == OpenMaya.MFn.kTypedAttribute + + mattr = OpenMaya.MFnTypedAttribute(mobject) + assert mattr.attrType() == OpenMaya.MFnData.kString + assert mattr.usedAsFilename is True + + +def test_create_curve(network_node: OpenMaya.MFnDependencyNode) -> None: + attribute = Curve("foo") + + modifier = OpenMaya.MDGModifier() + plug = add_attribute(network_node, attribute, modifier) + + mobject = cast(OpenMaya.MObject, plug.attribute()) + assert mobject.apiType() == OpenMaya.MFn.kTypedAttribute + + mattr = OpenMaya.MFnTypedAttribute(mobject) + assert mattr.attrType() == OpenMaya.MFnData.kNurbsCurve + + +def test_create_bool(network_node: OpenMaya.MFnDependencyNode) -> None: + attribute = Bool("foo", default=False) + + modifier = OpenMaya.MDGModifier() + plug = add_attribute(network_node, attribute, modifier) + assert plug.asBool() is False + + mobject = cast(OpenMaya.MObject, plug.attribute()) + assert mobject.apiType() == OpenMaya.MFn.kNumericAttribute + + mattr = OpenMaya.MFnNumericAttribute(mobject) + assert mattr.numericType() == OpenMaya.MFnNumericData.kBoolean # type: ignore[attr-defined] + assert mattr.default is False + + +def test_create_compound(network_node: OpenMaya.MFnDependencyNode) -> None: + attribute = Compound("foo") + attribute.append(Bool("bar")) + + modifier = OpenMaya.MDGModifier() + plug = add_attribute(network_node, attribute, modifier) + + assert plug.isCompound + assert plug.numChildren() == 1 + + +def test_create_enum(network_node: OpenMaya.MFnDependencyNode) -> None: + attribute = Enum("foo", fields={1: "bar", 2: "baz"}, default=2) + + modifier = OpenMaya.MDGModifier() + plug = add_attribute(network_node, attribute, modifier) + assert plug.asShort() == 2 + + mobject = cast(OpenMaya.MObject, plug.attribute()) + assert mobject.apiType() == OpenMaya.MFn.kEnumAttribute + + +def test_enum_fields_from_list() -> None: + attribute = Enum("foo", fields=["bar", "baz"]) + assert attribute.fields == {0: "bar", 1: "baz"} + + +def test_enum_no_fields() -> None: + attribute = Enum("foo") + assert not attribute.fields + assert attribute.default == 0 + + +def test_enum_no_default_return_first_index() -> None: + attribute = Enum("foo", fields={10: "bar", 20: "baz"}) + assert attribute.default == 10 + + +def test_enum_set_unbound_default_raise_value_error() -> None: + with pytest.raises(ValueError, match="Invalid index `2`"): + Enum("foo", fields={1: "bar"}, default=2) + + attribute = Enum("foo", fields={1: "bar"}) + with pytest.raises(ValueError, match="Invalid index `2`"): + attribute.default = 2 + + +@pytest.mark.parametrize( + ("attr_cls", "default"), + [ + (Double, 1.5), + (Distance, 1.5), + (Angle, 1.5), + (Float, 1.5), + (Long, 1), + (Short, 1), + ], +) +def test_create_numeric( + attr_cls: type[NumericAttribute], + default: float, + network_node: OpenMaya.MFnDependencyNode, +) -> None: + attribute = attr_cls("foo", default=default) + + modifier = OpenMaya.MDGModifier() + plug = add_attribute(network_node, attribute, modifier) + assert plug.asDouble() == pytest.approx(default) + + +@pytest.mark.parametrize( + ("attr_cls", "default"), + [ + (Double2, (1.5, -2.4)), + (Double3, (1.5, -2.4, 0.7)), + (Double4, (1.5, -2.4, 0.7, 123)), + (Distance2, (1.5, -2.4)), + (Distance3, (1.5, -2.4, 0.7)), + (Angle2, (1.5, -2.4)), + (Angle3, (1.5, -2.4, 0.7)), + (Angle4, (1.5, -2.4, 0.7, 123)), + (Float2, (1.5, -2.4)), + (Float3, (1.5, -2.4, 0.7)), + (Color, (1.5, -2.4, 0.7)), + (Long2, (1, -2)), + (Long3, (1, -2, 0)), + (Short2, (1, -2)), + (Short3, (1, -2, 0)), + ], +) +def test_create_numeric_compound( + attr_cls: type[NumericCompoundAttribute], + default: tuple, + network_node: OpenMaya.MFnDependencyNode, +) -> None: + attribute = attr_cls("foo", default=default) + + modifier = OpenMaya.MDGModifier() + plug = add_attribute(network_node, attribute, modifier) + + assert plug.isCompound + assert plug.numChildren() == len(default) + + children: list[OpenMaya.MPlug] = [plug.child(i) for i in range(plug.numChildren())] + assert tuple(c.asDouble() for c in children) == pytest.approx(default) diff --git a/tests/test_matrix_property.py b/tests/test_matrix_property.py new file mode 100644 index 0000000..8bd21cc --- /dev/null +++ b/tests/test_matrix_property.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from typing import Any + +import pytest +from maya.api import OpenMaya + +from attribs.base import MatrixProperty + + +def test_matrix_property_default_returns_identity_mmatrix() -> None: + prop = MatrixProperty() + obj = type("Foo", (), {"bar": prop})() + assert prop.__get__(obj, obj.__class__) == OpenMaya.MMatrix.kIdentity + + +@pytest.mark.parametrize( + ("value"), + [ + pytest.param( + [1, 2, 2, 0, 2, 1, 2, 0, 2, 2, 1, 0, 2, 2, 2, 1], + id="List", + ), + pytest.param( + OpenMaya.MMatrix([1, 2, 2, 0, 2, 1, 2, 0, 2, 2, 1, 0, 2, 2, 2, 1]), + id="MMatrix", + ), + pytest.param( + OpenMaya.MTransformationMatrix( + OpenMaya.MMatrix([1, 2, 2, 0, 2, 1, 2, 0, 2, 2, 1, 0, 2, 2, 2, 1]), + ), + id="MTransformationMatrix", + ), + ], +) +def test_matrix_property_set_convert_to_mmatrix(value: Any) -> None: # noqa: ANN401 + prop = MatrixProperty() + obj = type("Foo", (), {"bar": prop})() + + prop.__set__(obj, value) + + expected = OpenMaya.MMatrix([1, 2, 2, 0, 2, 1, 2, 0, 2, 2, 1, 0, 2, 2, 2, 1]) + result = prop.__get__(obj, obj.__class__) + + assert result.isEquivalent(expected) + + +def test_matrix_property_set_wrong_type_raise_type_error() -> None: + prop = MatrixProperty() + obj = type("Foo", (), {"bar": prop})() + + with pytest.raises(TypeError): + prop.__set__(obj, 1) # type: ignore[arg-type] + + +def test_matrix_property_set_invalid_sequence_raise_value_error() -> None: + prop = MatrixProperty() + obj = type("Foo", (), {"bar": prop})() + + with pytest.raises(ValueError): + prop.__set__(obj, [1]) diff --git a/tests/test_numeric_compound_property.py b/tests/test_numeric_compound_property.py new file mode 100644 index 0000000..8c79643 --- /dev/null +++ b/tests/test_numeric_compound_property.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +import pytest + +from attribs.base import NumericCompoundProperty + + +def test_numeric_compound_property() -> None: + class Child: + _foo = 1 + + @property + def foo(self) -> int: + return self._foo + + @foo.setter + def foo(self, value: int) -> None: + if value == 0: + raise ValueError(str(value)) + self._foo = value + + class Parent: + foo = NumericCompoundProperty[tuple[int, int], tuple[int, int]]() + children = (Child(), Child()) + + assert Parent().foo == (1, 1) + + attr = Parent() + attr.foo = (12, 13) + assert attr.foo == (12, 13) + assert attr.children[0].foo == 12 + assert attr.children[1].foo == 13 + + with pytest.raises(ValueError): + attr.foo = (10, 0) + + assert attr.foo == (12, 13) diff --git a/tests/test_numeric_property.py b/tests/test_numeric_property.py new file mode 100644 index 0000000..0dec1e3 --- /dev/null +++ b/tests/test_numeric_property.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import pytest +from maya.api import OpenMaya + +from attribs.base import MayaNumeric, NumericProperty + + +@pytest.mark.parametrize( + ("numeric_cls", "unit", "value", "expected"), + [ + pytest.param(float, None, 1.2, 1.2, id="float"), + pytest.param(float, None, None, None, id="None"), + pytest.param( + OpenMaya.MAngle, + OpenMaya.MAngle.kDegrees, + OpenMaya.MAngle(0, OpenMaya.MAngle.kRadians), + OpenMaya.MAngle(0, OpenMaya.MAngle.kDegrees), + id="MAngle-radians-to-degrees", + ), + pytest.param( + OpenMaya.MAngle, + OpenMaya.MAngle.kRadians, + OpenMaya.MAngle(0, OpenMaya.MAngle.kRadians), + OpenMaya.MAngle(0, OpenMaya.MAngle.kRadians), + id="MAngle-radians-to-radians", + ), + pytest.param( + OpenMaya.MAngle, + OpenMaya.MAngle.kRadians, + OpenMaya.MAngle(0, OpenMaya.MAngle.kDegrees), + OpenMaya.MAngle(0, OpenMaya.MAngle.kRadians), + id="MAngle-degrees-to-radians", + ), + pytest.param( + OpenMaya.MAngle, + OpenMaya.MAngle.kRadians, + 0, + OpenMaya.MAngle(0, OpenMaya.MAngle.kRadians), + id="float-to-MAngle", + ), + ], +) +def test_numeric_property_convert_value( + numeric_cls: type[MayaNumeric], + unit: int | None, + value: MayaNumeric | None, + expected: MayaNumeric | None, +) -> None: + prop: NumericProperty[MayaNumeric] = NumericProperty() + obj = type( + "Foo", + (), + {"bar": prop, "NUMERIC_CLS": numeric_cls, "unit": unit}, + )() + + prop.__set__(obj, value) + + result = prop.__get__(obj, obj.__class__) + + if isinstance(expected, (float, int)): + assert result == expected + elif expected is None: + assert result is None + else: + assert result.value == expected.value + assert result.unit == expected.unit diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..92d44bf --- /dev/null +++ b/uv.lock @@ -0,0 +1,297 @@ +version = 1 +requires-python = ">=3.7" + +[[package]] +name = "attribs" +version = "0.1.dev2" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "coverage" }, + { name = "maya-stubs" }, + { name = "mypy" }, + { name = "pytest" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage", specifier = ">=7.2.7" }, + { name = "maya-stubs", specifier = ">=0.4.1" }, + { name = "mypy", specifier = ">=1.4.1" }, + { name = "pytest", specifier = ">=7.4.4" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/8b/421f30467e69ac0e414214856798d4bc32da1336df745e49e49ae5c1e2a8/coverage-7.2.7.tar.gz", hash = "sha256:924d94291ca674905fe9481f12294eb11f2d3d3fd1adb20314ba89e94f44ed59", size = 762575 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/24/be01e62a7bce89bcffe04729c540382caa5a06bee45ae42136c93e2499f5/coverage-7.2.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d39b5b4f2a66ccae8b7263ac3c8170994b65266797fb96cbbfd3fb5b23921db8", size = 200724 }, + { url = "https://files.pythonhosted.org/packages/3d/80/7060a445e1d2c9744b683dc935248613355657809d6c6b2716cdf4ca4766/coverage-7.2.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6d040ef7c9859bb11dfeb056ff5b3872436e3b5e401817d87a31e1750b9ae2fb", size = 201024 }, + { url = "https://files.pythonhosted.org/packages/b8/9d/926fce7e03dbfc653104c2d981c0fa71f0572a9ebd344d24c573bd6f7c4f/coverage-7.2.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba90a9563ba44a72fda2e85302c3abc71c5589cea608ca16c22b9804262aaeb6", size = 229528 }, + { url = "https://files.pythonhosted.org/packages/d1/3a/67f5d18f911abf96857f6f7e4df37ca840e38179e2cc9ab6c0b9c3380f19/coverage-7.2.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7d9405291c6928619403db1d10bd07888888ec1abcbd9748fdaa971d7d661b2", size = 227842 }, + { url = "https://files.pythonhosted.org/packages/b4/bd/1b2331e3a04f4cc9b7b332b1dd0f3a1261dfc4114f8479bebfcc2afee9e8/coverage-7.2.7-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31563e97dae5598556600466ad9beea39fb04e0229e61c12eaa206e0aa202063", size = 228717 }, + { url = "https://files.pythonhosted.org/packages/2b/86/3dbf9be43f8bf6a5ca28790a713e18902b2d884bc5fa9512823a81dff601/coverage-7.2.7-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ebba1cd308ef115925421d3e6a586e655ca5a77b5bf41e02eb0e4562a111f2d1", size = 234632 }, + { url = "https://files.pythonhosted.org/packages/91/e8/469ed808a782b9e8305a08bad8c6fa5f8e73e093bda6546c5aec68275bff/coverage-7.2.7-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:cb017fd1b2603ef59e374ba2063f593abe0fc45f2ad9abdde5b4d83bd922a353", size = 232875 }, + { url = "https://files.pythonhosted.org/packages/29/8f/4fad1c2ba98104425009efd7eaa19af9a7c797e92d40cd2ec026fa1f58cb/coverage-7.2.7-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d62a5c7dad11015c66fbb9d881bc4caa5b12f16292f857842d9d1871595f4495", size = 234094 }, + { url = "https://files.pythonhosted.org/packages/94/4e/d4e46a214ae857be3d7dc5de248ba43765f60daeb1ab077cb6c1536c7fba/coverage-7.2.7-cp310-cp310-win32.whl", hash = "sha256:ee57190f24fba796e36bb6d3aa8a8783c643d8fa9760c89f7a98ab5455fbf818", size = 203184 }, + { url = "https://files.pythonhosted.org/packages/1f/e9/d6730247d8dec2a3dddc520ebe11e2e860f0f98cee3639e23de6cf920255/coverage-7.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:f75f7168ab25dd93110c8a8117a22450c19976afbc44234cbf71481094c1b850", size = 204096 }, + { url = "https://files.pythonhosted.org/packages/c6/fa/529f55c9a1029c840bcc9109d5a15ff00478b7ff550a1ae361f8745f8ad5/coverage-7.2.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06a9a2be0b5b576c3f18f1a241f0473575c4a26021b52b2a85263a00f034d51f", size = 200895 }, + { url = "https://files.pythonhosted.org/packages/67/d7/cd8fe689b5743fffac516597a1222834c42b80686b99f5b44ef43ccc2a43/coverage-7.2.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5baa06420f837184130752b7c5ea0808762083bf3487b5038d68b012e5937dbe", size = 201120 }, + { url = "https://files.pythonhosted.org/packages/8c/95/16eed713202406ca0a37f8ac259bbf144c9d24f9b8097a8e6ead61da2dbb/coverage-7.2.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdec9e8cbf13a5bf63290fc6013d216a4c7232efb51548594ca3631a7f13c3a3", size = 233178 }, + { url = "https://files.pythonhosted.org/packages/c1/49/4d487e2ad5d54ed82ac1101e467e8994c09d6123c91b2a962145f3d262c2/coverage-7.2.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:52edc1a60c0d34afa421c9c37078817b2e67a392cab17d97283b64c5833f427f", size = 230754 }, + { url = "https://files.pythonhosted.org/packages/a7/cd/3ce94ad9d407a052dc2a74fbeb1c7947f442155b28264eb467ee78dea812/coverage-7.2.7-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63426706118b7f5cf6bb6c895dc215d8a418d5952544042c8a2d9fe87fcf09cb", size = 232558 }, + { url = "https://files.pythonhosted.org/packages/8f/a8/12cc7b261f3082cc299ab61f677f7e48d93e35ca5c3c2f7241ed5525ccea/coverage-7.2.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:afb17f84d56068a7c29f5fa37bfd38d5aba69e3304af08ee94da8ed5b0865833", size = 241509 }, + { url = "https://files.pythonhosted.org/packages/04/fa/43b55101f75a5e9115259e8be70ff9279921cb6b17f04c34a5702ff9b1f7/coverage-7.2.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:48c19d2159d433ccc99e729ceae7d5293fbffa0bdb94952d3579983d1c8c9d97", size = 239924 }, + { url = "https://files.pythonhosted.org/packages/68/5f/d2bd0f02aa3c3e0311986e625ccf97fdc511b52f4f1a063e4f37b624772f/coverage-7.2.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0e1f928eaf5469c11e886fe0885ad2bf1ec606434e79842a879277895a50942a", size = 240977 }, + { url = "https://files.pythonhosted.org/packages/ba/92/69c0722882643df4257ecc5437b83f4c17ba9e67f15dc6b77bad89b6982e/coverage-7.2.7-cp311-cp311-win32.whl", hash = "sha256:33d6d3ea29d5b3a1a632b3c4e4f4ecae24ef170b0b9ee493883f2df10039959a", size = 203168 }, + { url = "https://files.pythonhosted.org/packages/b1/96/c12ed0dfd4ec587f3739f53eb677b9007853fd486ccb0e7d5512a27bab2e/coverage-7.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:5b7540161790b2f28143191f5f8ec02fb132660ff175b7747b95dcb77ac26562", size = 204185 }, + { url = "https://files.pythonhosted.org/packages/ff/d5/52fa1891d1802ab2e1b346d37d349cb41cdd4fd03f724ebbf94e80577687/coverage-7.2.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2f67fe12b22cd130d34d0ef79206061bfb5eda52feb6ce0dba0644e20a03cf4", size = 201020 }, + { url = "https://files.pythonhosted.org/packages/24/df/6765898d54ea20e3197a26d26bb65b084deefadd77ce7de946b9c96dfdc5/coverage-7.2.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a342242fe22407f3c17f4b499276a02b01e80f861f1682ad1d95b04018e0c0d4", size = 233994 }, + { url = "https://files.pythonhosted.org/packages/15/81/b108a60bc758b448c151e5abceed027ed77a9523ecbc6b8a390938301841/coverage-7.2.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:171717c7cb6b453aebac9a2ef603699da237f341b38eebfee9be75d27dc38e01", size = 231358 }, + { url = "https://files.pythonhosted.org/packages/61/90/c76b9462f39897ebd8714faf21bc985b65c4e1ea6dff428ea9dc711ed0dd/coverage-7.2.7-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49969a9f7ffa086d973d91cec8d2e31080436ef0fb4a359cae927e742abfaaa6", size = 233316 }, + { url = "https://files.pythonhosted.org/packages/04/d6/8cba3bf346e8b1a4fb3f084df7d8cea25a6b6c56aaca1f2e53829be17e9e/coverage-7.2.7-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b46517c02ccd08092f4fa99f24c3b83d8f92f739b4657b0f146246a0ca6a831d", size = 240159 }, + { url = "https://files.pythonhosted.org/packages/6e/ea/4a252dc77ca0605b23d477729d139915e753ee89e4c9507630e12ad64a80/coverage-7.2.7-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:a3d33a6b3eae87ceaefa91ffdc130b5e8536182cd6dfdbfc1aa56b46ff8c86de", size = 238127 }, + { url = "https://files.pythonhosted.org/packages/9f/5c/d9760ac497c41f9c4841f5972d0edf05d50cad7814e86ee7d133ec4a0ac8/coverage-7.2.7-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:976b9c42fb2a43ebf304fa7d4a310e5f16cc99992f33eced91ef6f908bd8f33d", size = 239833 }, + { url = "https://files.pythonhosted.org/packages/69/8c/26a95b08059db1cbb01e4b0e6d40f2e9debb628c6ca86b78f625ceaf9bab/coverage-7.2.7-cp312-cp312-win32.whl", hash = "sha256:8de8bb0e5ad103888d65abef8bca41ab93721647590a3f740100cd65c3b00511", size = 203463 }, + { url = "https://files.pythonhosted.org/packages/b7/00/14b00a0748e9eda26e97be07a63cc911108844004687321ddcc213be956c/coverage-7.2.7-cp312-cp312-win_amd64.whl", hash = "sha256:9e31cb64d7de6b6f09702bb27c02d1904b3aebfca610c12772452c4e6c21a0d3", size = 204347 }, + { url = "https://files.pythonhosted.org/packages/80/d7/67937c80b8fd4c909fdac29292bc8b35d9505312cff6bcab41c53c5b1df6/coverage-7.2.7-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58c2ccc2f00ecb51253cbe5d8d7122a34590fac9646a960d1430d5b15321d95f", size = 200580 }, + { url = "https://files.pythonhosted.org/packages/7a/05/084864fa4bbf8106f44fb72a56e67e0cd372d3bf9d893be818338c81af5d/coverage-7.2.7-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d22656368f0e6189e24722214ed8d66b8022db19d182927b9a248a2a8a2f67eb", size = 226237 }, + { url = "https://files.pythonhosted.org/packages/67/a2/6fa66a50e6e894286d79a3564f42bd54a9bd27049dc0a63b26d9924f0aa3/coverage-7.2.7-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a895fcc7b15c3fc72beb43cdcbdf0ddb7d2ebc959edac9cef390b0d14f39f8a9", size = 224256 }, + { url = "https://files.pythonhosted.org/packages/e2/c0/73f139794c742840b9ab88e2e17fe14a3d4668a166ff95d812ac66c0829d/coverage-7.2.7-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84606b74eb7de6ff581a7915e2dab7a28a0517fbe1c9239eb227e1354064dcd", size = 225550 }, + { url = "https://files.pythonhosted.org/packages/03/ec/6f30b4e0c96ce03b0e64aec46b4af2a8c49b70d1b5d0d69577add757b946/coverage-7.2.7-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0a5f9e1dbd7fbe30196578ca36f3fba75376fb99888c395c5880b355e2875f8a", size = 232440 }, + { url = "https://files.pythonhosted.org/packages/22/c1/2f6c1b6f01a0996c9e067a9c780e1824351dbe17faae54388a4477e6d86f/coverage-7.2.7-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:419bfd2caae268623dd469eff96d510a920c90928b60f2073d79f8fe2bbc5959", size = 230897 }, + { url = "https://files.pythonhosted.org/packages/8d/d6/53e999ec1bf7498ca4bc5f3b8227eb61db39068d2de5dcc359dec5601b5a/coverage-7.2.7-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2aee274c46590717f38ae5e4650988d1af340fe06167546cc32fe2f58ed05b02", size = 232024 }, + { url = "https://files.pythonhosted.org/packages/e9/40/383305500d24122dbed73e505a4d6828f8f3356d1f68ab6d32c781754b81/coverage-7.2.7-cp37-cp37m-win32.whl", hash = "sha256:61b9a528fb348373c433e8966535074b802c7a5d7f23c4f421e6c6e2f1697a6f", size = 203293 }, + { url = "https://files.pythonhosted.org/packages/0e/bc/7e3a31534fabb043269f14fb64e2bb2733f85d4cf39e5bbc71357c57553a/coverage-7.2.7-cp37-cp37m-win_amd64.whl", hash = "sha256:b1c546aca0ca4d028901d825015dc8e4d56aac4b541877690eb76490f1dc8ed0", size = 204040 }, + { url = "https://files.pythonhosted.org/packages/c6/fc/be19131010930a6cf271da48202c8cc1d3f971f68c02fb2d3a78247f43dc/coverage-7.2.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:54b896376ab563bd38453cecb813c295cf347cf5906e8b41d340b0321a5433e5", size = 200689 }, + { url = "https://files.pythonhosted.org/packages/28/d7/9a8de57d87f4bbc6f9a6a5ded1eaac88a89bf71369bb935dac3c0cf2893e/coverage-7.2.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3d376df58cc111dc8e21e3b6e24606b5bb5dee6024f46a5abca99124b2229ef5", size = 200986 }, + { url = "https://files.pythonhosted.org/packages/c8/e4/e6182e4697665fb594a7f4e4f27cb3a4dd00c2e3d35c5c706765de8c7866/coverage-7.2.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e330fc79bd7207e46c7d7fd2bb4af2963f5f635703925543a70b99574b0fea9", size = 230648 }, + { url = "https://files.pythonhosted.org/packages/7b/e3/f552d5871943f747165b92a924055c5d6daa164ae659a13f9018e22f3990/coverage-7.2.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e9d683426464e4a252bf70c3498756055016f99ddaec3774bf368e76bbe02b6", size = 228511 }, + { url = "https://files.pythonhosted.org/packages/44/55/49f65ccdd4dfd6d5528e966b28c37caec64170c725af32ab312889d2f857/coverage-7.2.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d13c64ee2d33eccf7437961b6ea7ad8673e2be040b4f7fd4fd4d4d28d9ccb1e", size = 229852 }, + { url = "https://files.pythonhosted.org/packages/0d/31/340428c238eb506feb96d4fb5c9ea614db1149517f22cc7ab8c6035ef6d9/coverage-7.2.7-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b7aa5f8a41217360e600da646004f878250a0d6738bcdc11a0a39928d7dc2050", size = 235578 }, + { url = "https://files.pythonhosted.org/packages/dd/ce/97c1dd6592c908425622fe7f31c017d11cf0421729b09101d4de75bcadc8/coverage-7.2.7-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fa03bce9bfbeeef9f3b160a8bed39a221d82308b4152b27d82d8daa7041fee5", size = 234079 }, + { url = "https://files.pythonhosted.org/packages/de/a3/5a98dc9e239d0dc5f243ef5053d5b1bdcaa1dee27a691dfc12befeccf878/coverage-7.2.7-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:245167dd26180ab4c91d5e1496a30be4cd721a5cf2abf52974f965f10f11419f", size = 234991 }, + { url = "https://files.pythonhosted.org/packages/4a/fb/78986d3022e5ccf2d4370bc43a5fef8374f092b3c21d32499dee8e30b7b6/coverage-7.2.7-cp38-cp38-win32.whl", hash = "sha256:d2c2db7fd82e9b72937969bceac4d6ca89660db0a0967614ce2481e81a0b771e", size = 203160 }, + { url = "https://files.pythonhosted.org/packages/c3/1c/6b3c9c363fb1433c79128e0d692863deb761b1b78162494abb9e5c328bc0/coverage-7.2.7-cp38-cp38-win_amd64.whl", hash = "sha256:2e07b54284e381531c87f785f613b833569c14ecacdcb85d56b25c4622c16c3c", size = 204085 }, + { url = "https://files.pythonhosted.org/packages/88/da/495944ebf0ad246235a6bd523810d9f81981f9b81c6059ba1f56e943abe0/coverage-7.2.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:537891ae8ce59ef63d0123f7ac9e2ae0fc8b72c7ccbe5296fec45fd68967b6c9", size = 200725 }, + { url = "https://files.pythonhosted.org/packages/ca/0c/3dfeeb1006c44b911ee0ed915350db30325d01808525ae7cc8d57643a2ce/coverage-7.2.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:06fb182e69f33f6cd1d39a6c597294cff3143554b64b9825d1dc69d18cc2fff2", size = 201022 }, + { url = "https://files.pythonhosted.org/packages/61/af/5964b8d7d9a5c767785644d9a5a63cacba9a9c45cc42ba06d25895ec87be/coverage-7.2.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:201e7389591af40950a6480bd9edfa8ed04346ff80002cec1a66cac4549c1ad7", size = 229102 }, + { url = "https://files.pythonhosted.org/packages/d9/1d/cd467fceb62c371f9adb1d739c92a05d4e550246daa90412e711226bd320/coverage-7.2.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f6951407391b639504e3b3be51b7ba5f3528adbf1a8ac3302b687ecababf929e", size = 227441 }, + { url = "https://files.pythonhosted.org/packages/fe/57/e4f8ad64d84ca9e759d783a052795f62a9f9111585e46068845b1cb52c2b/coverage-7.2.7-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f48351d66575f535669306aa7d6d6f71bc43372473b54a832222803eb956fd1", size = 228265 }, + { url = "https://files.pythonhosted.org/packages/88/8b/b0d9fe727acae907fa7f1c8194ccb6fe9d02e1c3e9001ecf74c741f86110/coverage-7.2.7-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b29019c76039dc3c0fd815c41392a044ce555d9bcdd38b0fb60fb4cd8e475ba9", size = 234217 }, + { url = "https://files.pythonhosted.org/packages/66/2e/c99fe1f6396d93551aa352c75410686e726cd4ea104479b9af1af22367ce/coverage-7.2.7-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:81c13a1fc7468c40f13420732805a4c38a105d89848b7c10af65a90beff25250", size = 232466 }, + { url = "https://files.pythonhosted.org/packages/bb/e9/88747b40c8fb4a783b40222510ce6d66170217eb05d7f46462c36b4fa8cc/coverage-7.2.7-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:975d70ab7e3c80a3fe86001d8751f6778905ec723f5b110aed1e450da9d4b7f2", size = 233669 }, + { url = "https://files.pythonhosted.org/packages/b1/d5/a8e276bc005e42114468d4fe03e0a9555786bc51cbfe0d20827a46c1565a/coverage-7.2.7-cp39-cp39-win32.whl", hash = "sha256:7ee7d9d4822c8acc74a5e26c50604dff824710bc8de424904c0982e25c39c6cb", size = 203199 }, + { url = "https://files.pythonhosted.org/packages/a9/0c/4a848ae663b47f1195abcb09a951751dd61f80b503303b9b9d768e0fd321/coverage-7.2.7-cp39-cp39-win_amd64.whl", hash = "sha256:eb393e5ebc85245347950143969b241d08b52b88a3dc39479822e073a1a8eb27", size = 204109 }, + { url = "https://files.pythonhosted.org/packages/67/fb/b3b1d7887e1ea25a9608b0776e480e4bbc303ca95a31fd585555ec4fff5a/coverage-7.2.7-pp37.pp38.pp39-none-any.whl", hash = "sha256:b7b4c971f05e6ae490fef852c218b0e79d4e52f79ef0c8475566584a8fb3e01d", size = 193207 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "importlib-metadata" +version = "6.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.8'" }, + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/82/f6e29c8d5c098b6be61460371c2c5591f4a335923639edec43b3830650a4/importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4", size = 53569 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/94/64287b38c7de4c90683630338cf28f129decbba0a44f0c6db35a873c73c4/importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5", size = 22934 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "maya-stubs" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/76/22b910b2ebbcc306edac24c248fecc78591398a8d8500fa1c53263ea2973/maya_stubs-0.4.1.tar.gz", hash = "sha256:d9b8c515da2a446fdc493853a7dacb2758183208aef2f6f13e190c3d009cc8d5", size = 1358046 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/ce/3b4b049140e19df5735adee90087db05f46558dad7a46ada684318c0e63a/maya_stubs-0.4.1-py2.py3-none-any.whl", hash = "sha256:d17e4b5bfced49d333991a1df0815c3eb159fce88fce0b23cb5e10d53d3480a7", size = 1376766 }, +] + +[[package]] +name = "mypy" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typed-ast", marker = "python_full_version < '3.8'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/28/d8a8233ff167d06108e53b7aefb4a8d7350adbbf9d7abd980f17fdb7a3a6/mypy-1.4.1.tar.gz", hash = "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b", size = 2855162 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/3b/1c7363863b56c059f60a1dfdca9ac774a22ba64b7a4da0ee58ee53e5243f/mypy-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8", size = 10451043 }, + { url = "https://files.pythonhosted.org/packages/a7/24/6f0df1874118839db1155fed62a4bd7e80c181367ff8ea07d40fbaffcfb4/mypy-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878", size = 9542079 }, + { url = "https://files.pythonhosted.org/packages/04/5c/deeac94fcccd11aa621e6b350df333e1b809b11443774ea67582cc0205da/mypy-1.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd", size = 11974913 }, + { url = "https://files.pythonhosted.org/packages/e5/2f/de3c455c54e8cf5e37ea38705c1920f2df470389f8fc051084d2dd8c9c59/mypy-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc", size = 12044492 }, + { url = "https://files.pythonhosted.org/packages/e7/d3/6f65357dcb68109946de70cd55bd2e60f10114f387471302f48d54ff5dae/mypy-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1", size = 8831655 }, + { url = "https://files.pythonhosted.org/packages/94/01/e34e37a044325af4d4af9825c15e8a0d26d89b5a9624b4d0908449d3411b/mypy-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462", size = 10338636 }, + { url = "https://files.pythonhosted.org/packages/92/58/ccc0b714ecbd1a64b34d8ce1c38763ff6431de1d82551904ecc3711fbe05/mypy-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258", size = 9444172 }, + { url = "https://files.pythonhosted.org/packages/73/72/dfc0b46e6905eafd598e7c48c0c4f2e232647e4e36547425c64e6c850495/mypy-1.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2", size = 11855450 }, + { url = "https://files.pythonhosted.org/packages/66/f4/60739a2d336f3adf5628e7c9b920d16e8af6dc078550d615e4ba2a1d7759/mypy-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7", size = 11928679 }, + { url = "https://files.pythonhosted.org/packages/8c/26/6ff2b55bf8b605a4cc898883654c2ca4dd4feedf0bb04ecaacf60d165cde/mypy-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01", size = 8831134 }, + { url = "https://files.pythonhosted.org/packages/95/47/fb69dad9634af9f1dab69f8b4031d674592384b59c7171852b1fbed6de15/mypy-1.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b", size = 10101278 }, + { url = "https://files.pythonhosted.org/packages/65/f7/77339904a3415cadca5551f2ea0c74feefc9b7187636a292690788f4d4b3/mypy-1.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b", size = 11643877 }, + { url = "https://files.pythonhosted.org/packages/f5/93/ae39163ae84266d24d1fcf8ee1e2db1e0346e09de97570dd101a07ccf876/mypy-1.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7", size = 11702718 }, + { url = "https://files.pythonhosted.org/packages/13/3b/3b7de921626547b36c34b91c74cfbda260210df7c49bd3d315015cfd6005/mypy-1.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9", size = 8551181 }, + { url = "https://files.pythonhosted.org/packages/49/7d/63bab763e4d44e1a7c341fb64496ddf20970780935596ffed9ed2d85eae7/mypy-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042", size = 10390236 }, + { url = "https://files.pythonhosted.org/packages/23/3f/54a87d933440416a1efd7a42b45f8cf22e353efe889eb3903cc34177ab44/mypy-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3", size = 9496760 }, + { url = "https://files.pythonhosted.org/packages/4e/89/26230b46e27724bd54f76cd73a2759eaaf35292b32ba64f36c7c47836d4b/mypy-1.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6", size = 11927489 }, + { url = "https://files.pythonhosted.org/packages/64/7d/156e721376951c449554942eedf4d53e9ca2a57e94bf0833ad2821d59bfa/mypy-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f", size = 11990009 }, + { url = "https://files.pythonhosted.org/packages/27/ab/21230851e8137c9ef9a095cc8cb70d8ff8cac21014e4b249ac7a9eae7df9/mypy-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc", size = 8816535 }, + { url = "https://files.pythonhosted.org/packages/1d/1b/9050b5c444ef82c3d59bdbf21f91b259cf20b2ac1df37d55bc6b91d609a1/mypy-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828", size = 10447897 }, + { url = "https://files.pythonhosted.org/packages/da/00/ac2b58b321d85cac25be0dcd1bc2427dfc6cf403283fc205a0031576f14b/mypy-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3", size = 9534091 }, + { url = "https://files.pythonhosted.org/packages/c4/10/26240f14e854a95af87d577b288d607ebe0ccb75cb37052f6386402f022d/mypy-1.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816", size = 11970165 }, + { url = "https://files.pythonhosted.org/packages/b7/34/a3edaec8762181bfe97439c7e094f4c2f411ed9b79ac8f4d72156e88d5ce/mypy-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c", size = 12040792 }, + { url = "https://files.pythonhosted.org/packages/d1/f3/0d0622d5a83859a992b01741a7b97949d6fb9efc9f05f20a09f0df10dc1e/mypy-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f", size = 8831367 }, + { url = "https://files.pythonhosted.org/packages/3d/9a/e13addb8d652cb068f835ac2746d9d42f85b730092f581bb17e2059c28f1/mypy-1.4.1-py3-none-any.whl", hash = "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4", size = 2451741 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "packaging" +version = "24.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488 }, +] + +[[package]] +name = "pluggy" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.8'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/42/8f2833655a29c4e9cb52ee8a2be04ceac61bcff4a680fb338cbd3d1e322d/pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3", size = 61613 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/32/4a79112b8b87b21450b066e102d6608907f4c885ed7b04c3fdb085d4d6ae/pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", size = 17695 }, +] + +[[package]] +name = "pytest" +version = "7.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "importlib-metadata", marker = "python_full_version < '3.8'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287 }, +] + +[[package]] +name = "tomli" +version = "2.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757 }, +] + +[[package]] +name = "typed-ast" +version = "1.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/7e/a424029f350aa8078b75fd0d360a787a273ca753a678d1104c5fa4f3072a/typed_ast-1.5.5.tar.gz", hash = "sha256:94282f7a354f36ef5dbce0ef3467ebf6a258e370ab33d5b40c249fa996e590dd", size = 252841 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/07/5defe18d4fc16281cd18c4374270abc430c3d852d8ac29b5db6599d45cfe/typed_ast-1.5.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4bc1efe0ce3ffb74784e06460f01a223ac1f6ab31c6bc0376a21184bf5aabe3b", size = 223267 }, + { url = "https://files.pythonhosted.org/packages/a0/5c/e379b00028680bfcd267d845cf46b60e76d8ac6f7009fd440d6ce030cc92/typed_ast-1.5.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f7a8c46a8b333f71abd61d7ab9255440d4a588f34a21f126bbfc95f6049e686", size = 208260 }, + { url = "https://files.pythonhosted.org/packages/3b/99/5cc31ef4f3c80e1ceb03ed2690c7085571e3fbf119cbd67a111ec0b6622f/typed_ast-1.5.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597fc66b4162f959ee6a96b978c0435bd63791e31e4f410622d19f1686d5e769", size = 842272 }, + { url = "https://files.pythonhosted.org/packages/e2/ed/b9b8b794b37b55c9247b1e8d38b0361e8158795c181636d34d6c11b506e7/typed_ast-1.5.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d41b7a686ce653e06c2609075d397ebd5b969d821b9797d029fccd71fdec8e04", size = 824651 }, + { url = "https://files.pythonhosted.org/packages/ca/59/dbbbe5a0e91c15d14a0896b539a5ed01326b0d468e75c1a33274d128d2d1/typed_ast-1.5.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5fe83a9a44c4ce67c796a1b466c270c1272e176603d5e06f6afbc101a572859d", size = 854960 }, + { url = "https://files.pythonhosted.org/packages/90/f0/0956d925f87bd81f6e0f8cf119eac5e5c8f4da50ca25bb9f5904148d4611/typed_ast-1.5.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d5c0c112a74c0e5db2c75882a0adf3133adedcdbfd8cf7c9d6ed77365ab90a1d", size = 839321 }, + { url = "https://files.pythonhosted.org/packages/43/17/4bdece9795da6f3345c4da5667ac64bc25863617f19c28d81f350f515be6/typed_ast-1.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:e1a976ed4cc2d71bb073e1b2a250892a6e968ff02aa14c1f40eba4f365ffec02", size = 139380 }, + { url = "https://files.pythonhosted.org/packages/75/53/b685e10da535c7b3572735f8bea0d4abb35a04722a7d44ca9c163a0cf822/typed_ast-1.5.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c631da9710271cb67b08bd3f3813b7af7f4c69c319b75475436fcab8c3d21bee", size = 223264 }, + { url = "https://files.pythonhosted.org/packages/96/fd/fc8ccf19fc16a40a23e7c7802d0abc78c1f38f1abb6e2447c474f8a076d8/typed_ast-1.5.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b445c2abfecab89a932b20bd8261488d574591173d07827c1eda32c457358b18", size = 208158 }, + { url = "https://files.pythonhosted.org/packages/bf/9a/598e47f2c3ecd19d7f1bb66854d0d3ba23ffd93c846448790a92524b0a8d/typed_ast-1.5.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc95ffaaab2be3b25eb938779e43f513e0e538a84dd14a5d844b8f2932593d88", size = 878366 }, + { url = "https://files.pythonhosted.org/packages/60/ca/765e8bf8b24d0ed7b9fc669f6826c5bc3eb7412fc765691f59b83ae195b2/typed_ast-1.5.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61443214d9b4c660dcf4b5307f15c12cb30bdfe9588ce6158f4a005baeb167b2", size = 860314 }, + { url = "https://files.pythonhosted.org/packages/d9/3c/4af750e6c673a0dd6c7b9f5b5e5ed58ec51a2e4e744081781c664d369dfa/typed_ast-1.5.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6eb936d107e4d474940469e8ec5b380c9b329b5f08b78282d46baeebd3692dc9", size = 898108 }, + { url = "https://files.pythonhosted.org/packages/03/8d/d0a4d1e060e1e8dda2408131a0cc7633fc4bc99fca5941dcb86c461dfe01/typed_ast-1.5.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e48bf27022897577d8479eaed64701ecaf0467182448bd95759883300ca818c8", size = 881971 }, + { url = "https://files.pythonhosted.org/packages/90/83/f28d2c912cd010a09b3677ac69d23181045eb17e358914ab739b7fdee530/typed_ast-1.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:83509f9324011c9a39faaef0922c6f720f9623afe3fe220b6d0b15638247206b", size = 139286 }, + { url = "https://files.pythonhosted.org/packages/d5/00/635353c31b71ed307ab020eff6baed9987da59a1b2ba489f885ecbe293b8/typed_ast-1.5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:2188bc33d85951ea4ddad55d2b35598b2709d122c11c75cffd529fbc9965508e", size = 222315 }, + { url = "https://files.pythonhosted.org/packages/01/95/11be104446bb20212a741d30d40eab52a9cfc05ea34efa074ff4f7c16983/typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0635900d16ae133cab3b26c607586131269f88266954eb04ec31535c9a12ef1e", size = 793541 }, + { url = "https://files.pythonhosted.org/packages/32/f1/75bd58fb1410cb72fbc6e8adf163015720db2c38844b46a9149c5ff6bf38/typed_ast-1.5.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57bfc3cf35a0f2fdf0a88a3044aafaec1d2f24d8ae8cd87c4f58d615fb5b6311", size = 778348 }, + { url = "https://files.pythonhosted.org/packages/47/97/0bb4dba688a58ff9c08e63b39653e4bcaa340ce1bb9c1d58163e5c2c66f1/typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:fe58ef6a764de7b4b36edfc8592641f56e69b7163bba9f9c8089838ee596bfb2", size = 809447 }, + { url = "https://files.pythonhosted.org/packages/a8/cd/9a867f5a96d83a9742c43914e10d3a2083d8fe894ab9bf60fd467c6c497f/typed_ast-1.5.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d09d930c2d1d621f717bb217bf1fe2584616febb5138d9b3e8cdd26506c3f6d4", size = 796707 }, + { url = "https://files.pythonhosted.org/packages/eb/06/73ca55ee5303b41d08920de775f02d2a3e1e59430371f5adf7fbb1a21127/typed_ast-1.5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:d40c10326893ecab8a80a53039164a224984339b2c32a6baf55ecbd5b1df6431", size = 138403 }, + { url = "https://files.pythonhosted.org/packages/19/e3/88b65e46643006592f39e0fdef3e29454244a9fdaa52acfb047dc68cae6a/typed_ast-1.5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fd946abf3c31fb50eee07451a6aedbfff912fcd13cf357363f5b4e834cc5e71a", size = 222951 }, + { url = "https://files.pythonhosted.org/packages/15/e0/182bdd9edb6c6a1c068cecaa87f58924a817f2807a0b0d940f578b3328df/typed_ast-1.5.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ed4a1a42df8a3dfb6b40c3d2de109e935949f2f66b19703eafade03173f8f437", size = 208247 }, + { url = "https://files.pythonhosted.org/packages/8d/09/bba083f2c11746288eaf1859e512130420405033de84189375fe65d839ba/typed_ast-1.5.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:045f9930a1550d9352464e5149710d56a2aed23a2ffe78946478f7b5416f1ede", size = 861010 }, + { url = "https://files.pythonhosted.org/packages/31/f3/38839df509b04fb54205e388fc04b47627377e0ad628870112086864a441/typed_ast-1.5.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:381eed9c95484ceef5ced626355fdc0765ab51d8553fec08661dce654a935db4", size = 840026 }, + { url = "https://files.pythonhosted.org/packages/45/1e/aa5f1dae4b92bc665ae9a655787bb2fe007a881fa2866b0408ce548bb24c/typed_ast-1.5.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:bfd39a41c0ef6f31684daff53befddae608f9daf6957140228a08e51f312d7e6", size = 875615 }, + { url = "https://files.pythonhosted.org/packages/94/88/71a1c249c01fbbd66f9f28648f8249e737a7fe19056c1a78e7b3b9250eb1/typed_ast-1.5.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8c524eb3024edcc04e288db9541fe1f438f82d281e591c548903d5b77ad1ddd4", size = 858320 }, + { url = "https://files.pythonhosted.org/packages/12/1e/19f53aad3984e351e6730e4265fde4b949a66c451e10828fdbc4dfb050f1/typed_ast-1.5.5-cp38-cp38-win_amd64.whl", hash = "sha256:7f58fabdde8dcbe764cef5e1a7fcb440f2463c1bbbec1cf2a86ca7bc1f95184b", size = 139414 }, + { url = "https://files.pythonhosted.org/packages/b1/88/6e7f36f5fab6fbf0586a2dd866ac337924b7d4796a4d1b2b04443a864faf/typed_ast-1.5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:042eb665ff6bf020dd2243307d11ed626306b82812aba21836096d229fdc6a10", size = 223329 }, + { url = "https://files.pythonhosted.org/packages/71/30/09d27e13824495547bcc665bd07afc593b22b9484f143b27565eae4ccaac/typed_ast-1.5.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:622e4a006472b05cf6ef7f9f2636edc51bda670b7bbffa18d26b255269d3d814", size = 208314 }, + { url = "https://files.pythonhosted.org/packages/07/3d/564308b7a432acb1f5399933cbb1b376a1a64d2544b90f6ba91894674260/typed_ast-1.5.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1efebbbf4604ad1283e963e8915daa240cb4bf5067053cf2f0baadc4d4fb51b8", size = 840900 }, + { url = "https://files.pythonhosted.org/packages/ea/f4/262512d14f777ea3666a089e2675a9b1500a85b8329a36de85d63433fb0e/typed_ast-1.5.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0aefdd66f1784c58f65b502b6cf8b121544680456d1cebbd300c2c813899274", size = 823435 }, + { url = "https://files.pythonhosted.org/packages/a1/25/b3ccb948166d309ab75296ac9863ebe2ff209fbc063f1122a2d3979e47c3/typed_ast-1.5.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:48074261a842acf825af1968cd912f6f21357316080ebaca5f19abbb11690c8a", size = 853125 }, + { url = "https://files.pythonhosted.org/packages/1c/09/012da182242f168bb5c42284297dcc08dc0a1b3668db5b3852aec467f56f/typed_ast-1.5.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:429ae404f69dc94b9361bb62291885894b7c6fb4640d561179548c849f8492ba", size = 837280 }, + { url = "https://files.pythonhosted.org/packages/30/bd/c815051404c4293265634d9d3e292f04fcf681d0502a9484c38b8f224d04/typed_ast-1.5.5-cp39-cp39-win_amd64.whl", hash = "sha256:335f22ccb244da2b5c296e6f96b06ee9bed46526db0de38d2f0e5a6597b81155", size = 139486 }, +] + +[[package]] +name = "typing-extensions" +version = "4.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/8b/0111dd7d6c1478bf83baa1cab85c686426c7a6274119aceb2bd9d35395ad/typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2", size = 72876 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6b/63cc3df74987c36fe26157ee12e09e8f9db4de771e0f3404263117e75b95/typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", size = 33232 }, +] + +[[package]] +name = "zipp" +version = "3.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/27/f0ac6b846684cecce1ee93d32450c45ab607f65c2e0255f0092032d91f07/zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", size = 18454 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/fa/c9e82bbe1af6266adf08afb563905eb87cab83fde00a0a08963510621047/zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556", size = 6758 }, +]