diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000000..dee1e99ff74 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(grep:*)", + "Bash(uv run pytest:*)" + ], + "deny": [] + }, + "outputStyle": "default" +} diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index bef8c317bb9..4c69d63a236 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -160,10 +160,15 @@ def getfuncargnames( if not any(p.kind is Parameter.POSITIONAL_ONLY for p in parameters) and ( # Not using `getattr` because we don't want to resolve the staticmethod. # Not using `cls.__dict__` because we want to check the entire MRO. - cls - and not isinstance( - inspect.getattr_static(cls, name, default=None), staticmethod + ( + cls + and not isinstance( + inspect.getattr_static(cls, name, default=None), staticmethod + ) ) + or + # Handle bound methods (e.g., from plugin instances) + hasattr(function, "__self__") ): arg_names = arg_names[1:] # Remove any names that will be replaced with mocks. diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index cd255f5eeb6..a0b76e95ce3 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -132,9 +132,9 @@ def pytest_collect_file( if config.option.doctestmodules and not any( (_is_setup_py(file_path), _is_main_py(file_path)) ): - return DoctestModule.from_parent(parent, path=file_path) + return DoctestModule.from_parent(parent, path=file_path) # type: ignore[no-any-return] elif _is_doctest(config, file_path, parent): - return DoctestTextfile.from_parent(parent, path=file_path) + return DoctestTextfile.from_parent(parent, path=file_path) # type: ignore[no-any-return] return None @@ -418,7 +418,7 @@ def _get_continue_on_failure(config: Config) -> bool: class DoctestTextfile(Module): - obj = None + obj: None = None # type: ignore[assignment] def collect(self) -> Iterable[DoctestItem]: import doctest @@ -544,13 +544,24 @@ def _from_module(self, module, object): # Type ignored because this is a private function. return super()._from_module(module, object) # type: ignore[misc] - try: - module = self.obj - except Collector.CollectError: - if self.config.getvalue("doctest_ignore_import_errors"): - skip(f"unable to import module {self.path!r}") - else: - raise + # Ensure the module is imported (similar to Module.collect) + if not hasattr(self, "obj") or self.obj is None: + from _pytest.python import importtestmodule + + try: + self.obj = importtestmodule(self.path, self.config) + # Extract markers from the module after importing + if self._ALLOW_MARKERS: + from _pytest.mark.structures import get_unpacked_marks + + self.own_markers.extend(get_unpacked_marks(self.obj)) + except Collector.CollectError: + if self.config.getvalue("doctest_ignore_import_errors"): + skip(f"unable to import module {self.path!r}") + else: + raise + + module = self.obj # While doctests currently don't support fixtures directly, we still # need to pick up autouse fixtures. diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 91f1b3a67f6..b97d57d6123 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -402,6 +402,8 @@ def __init__( # - In the future we might consider using a generic for the param type, but # for now just using Any. self.param: Any + # Instance for test methods (created lazily) + self._instance: Any | None = None @property def _fixturemanager(self) -> FixtureManager: @@ -466,7 +468,25 @@ def instance(self): """Instance (can be None) on which test function was collected.""" if self.scope != "function": return None - return getattr(self._pyfuncitem, "instance", None) + + # For SubRequest, delegate to parent request to share the instance + if isinstance(self, SubRequest) and hasattr(self, "_parent_request"): + return self._parent_request.instance + + # If the pyfuncitem has its own instance property (e.g., TestCaseFunction), use it + if hasattr(self._pyfuncitem, "instance"): + pyfuncitem_instance = getattr(self._pyfuncitem, "instance", None) + if pyfuncitem_instance is not None: + return pyfuncitem_instance + + # Check if this is a method that needs an instance + clscol = self._pyfuncitem.getparent(_pytest.python.Class) + if not clscol: + return None + # Create instance lazily if not already created + if self._instance is None: + self._instance = clscol.obj() + return self._instance @property def module(self): @@ -711,6 +731,10 @@ def __repr__(self) -> str: def _fillfixtures(self) -> None: item = self._pyfuncitem + # Ensure instance exists if this is a test method + if self.instance is not None: + # Instance has been created by the property getter + pass for argname in item.fixturenames: if argname not in item.funcargs: item.funcargs[argname] = self.getfixturevalue(argname) @@ -910,8 +934,68 @@ def toterminal(self, tw: TerminalWriter) -> None: def call_fixture_func( - fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs + fixturefunc: _FixtureFunc[FixtureValue], + request: FixtureRequest, + kwargs, + fixturedef: FixtureDef[FixtureValue] | None = None, ) -> FixtureValue: + # Check if this is an unbound method that needs self + # This happens for high-scope (session/module) fixtures that are instance methods + if fixturedef is not None: + expects_self = getattr(fixturedef, "_expects_self", False) + is_classmethod = getattr(fixturedef, "_is_classmethod", False) + is_staticmethod = isinstance(fixturedef.func, staticmethod) + + # Check if the function is already a bound method (from a plugin instance) + is_bound_method = inspect.ismethod(fixturefunc) + + # If it's an instance method but we don't have an instance, we need to provide self + # BUT skip this if the method is already bound (from plugin instances) + if ( + expects_self + and not is_classmethod + and not is_staticmethod + and request.instance is None + and not is_bound_method + ): + # Get or create an instance for this fixture + # Look for the class from the fixture's qualname + qualname = getattr(fixturedef.func, "__qualname__", "") + if "." in qualname: + class_name = qualname.split(".")[-2] + module = getattr(fixturedef.func, "__module__", None) + if module: + import sys + + if module in sys.modules: + mod = sys.modules[module] + if hasattr(mod, class_name): + cls = getattr(mod, class_name) + # Create instance and call method + instance = cls() + if inspect.isgeneratorfunction(fixturefunc): + fixturefunc = cast( + Callable[..., Generator[FixtureValue]], fixturefunc + ) + generator = fixturefunc(instance, **kwargs) + try: + fixture_result = next(generator) + except StopIteration: + raise ValueError( + f"{request.fixturename} did not yield a value" + ) from None + finalizer = functools.partial( + _teardown_yield_fixture, fixturefunc, generator + ) + request.addfinalizer(finalizer) + return fixture_result + else: + fixturefunc = cast( + Callable[..., FixtureValue], fixturefunc + ) + return fixturefunc(instance, **kwargs) + + # Normal case - fixture is bound or doesn't need self if inspect.isgeneratorfunction(fixturefunc): fixturefunc = cast(Callable[..., Generator[FixtureValue]], fixturefunc) generator = fixturefunc(**kwargs) @@ -1029,7 +1113,76 @@ def __init__( # a parameter value. self.ids: Final = ids # The names requested by the fixtures. - self.argnames: Final = getfuncargnames(func, name=argname) + argnames = getfuncargnames(func, name=argname) + + # Determine the type of method and handle first parameter accordingly + is_classmethod = isinstance(func, classmethod) + is_staticmethod = isinstance(func, staticmethod) + + # Check if the function is already bound (has __self__) + # This happens for plugin fixtures where an instance method is registered + is_bound_method = hasattr(func, "__self__") + + # Check if this is actually a method defined in the class + # (not just a fixture registered from a class context) + is_class_method = False + if ( + not is_bound_method + and baseid + and "::" in baseid + and hasattr(func, "__name__") + ): + # Extract the class from baseid and check if func is actually a method of that class + # Only filter first param for actual class methods, not for fixtures registered by the class + # Check if func is a method by seeing if it has __qualname__ indicating it's defined in a class + qualname = getattr(func, "__qualname__", "") + # If qualname contains a dot, it's a method (e.g., "TestClass.method_name") + # xunit fixtures have qualname like "Class._register_setup_method_fixture..xunit_setup_method_fixture" + # which contains "" indicating it's a nested function, not a class method + is_class_method = "." in qualname and "" not in qualname + + # Check if first parameter is "cls" - this indicates a classmethod even if + # it's been wrapped by the fixture decorator + if not is_classmethod and is_class_method and argnames and argnames[0] == "cls": + is_classmethod = True + + # For classmethods and instance methods defined in classes, + # the first parameter is not a fixture (it's cls or self) + # We detect this based on the method type, not parameter name + final_argnames: tuple[str, ...] + final_expects_self: bool + final_is_classmethod: bool + + if is_bound_method: + # Already bound method (e.g., plugin fixtures) - self is already bound + # Don't filter any parameters + final_argnames = argnames + final_expects_self = False + final_is_classmethod = False + elif is_classmethod: + # Classmethod: first param is cls (regardless of actual name) + final_argnames = argnames[1:] if argnames else () + final_expects_self = True + final_is_classmethod = True + elif is_staticmethod: + # Staticmethod: no special first parameter + final_argnames = argnames + final_expects_self = False + final_is_classmethod = False + elif is_class_method and argnames: + # Instance method in a class - first parameter is self (regardless of actual name) + final_argnames = argnames[1:] if argnames else () + final_expects_self = True + final_is_classmethod = False + else: + # Regular function, not a method + final_argnames = argnames + final_expects_self = False + final_is_classmethod = False + + self.argnames: Final = final_argnames + self._expects_self: Final = final_expects_self + self._is_classmethod: Final = final_is_classmethod # If the fixture was executed, the current value of the fixture. # Can change if the fixture is executed with different parameters. self.cached_result: _FixtureCachedResult[FixtureValue] | None = None @@ -1136,28 +1289,153 @@ def __repr__(self) -> str: return f"" +def bind_fixture_function( + func: Any, + instance: object | None, + is_classmethod: bool = False, + is_staticmethod: bool = False, + expects_self: bool = False, +) -> Any: + """Bind a fixture function to an instance or class as needed. + + This provides a single abstraction for handling: + - Regular functions (no binding) + - Instance methods (bind to instance) + - Class methods (bind to class) + - Static methods (no binding) + + Args: + func: The function to potentially bind + instance: The instance to bind to (if any) + is_classmethod: Whether this is a classmethod + is_staticmethod: Whether this is a staticmethod + expects_self: Whether the function expects a self/cls parameter + + Returns: + The appropriately bound (or unbound) function + """ + # Static methods never need binding + if is_staticmethod: + if isinstance(func, staticmethod): + return func.__func__ + return func + + # Get the underlying function + if isinstance(func, classmethod): + base_func = func.__func__ + else: + base_func = getimfunc(func) + + # No binding needed if function doesn't expect self/cls + if not expects_self: + return base_func + + # Class methods bind to the class + if is_classmethod: + if instance is not None: + return base_func.__get__(None, instance.__class__) + # No instance, can't bind classmethod properly + # This shouldn't normally happen + return base_func + + # Instance methods bind to the instance + if instance is not None: + return base_func.__get__(instance) + + # Instance method but no instance - can't bind + return base_func + + def resolve_fixture_function( fixturedef: FixtureDef[FixtureValue], request: FixtureRequest ) -> _FixtureFunc[FixtureValue]: """Get the actual callable that can be called to obtain the fixture value.""" fixturefunc = fixturedef.func - # The fixture function needs to be bound to the actual - # request.instance so that code working with "fixturedef" behaves - # as expected. instance = request.instance - if instance is not None: - # Handle the case where fixture is defined not in a test class, but some other class - # (for example a plugin class with a fixture), see #2270. - if hasattr(fixturefunc, "__self__") and not isinstance( - instance, - fixturefunc.__self__.__class__, - ): + + # Handle the case where fixture is defined not in a test class, but some other class + # (for example a plugin class with a fixture), see #2270. + if instance is not None and hasattr(fixturefunc, "__self__"): + if not isinstance(instance, fixturefunc.__self__.__class__): return fixturefunc - fixturefunc = getimfunc(fixturedef.func) - if fixturefunc != fixturedef.func: - fixturefunc = fixturefunc.__get__(instance) - return fixturefunc + + # Check fixture attributes set during initialization + is_classmethod = getattr(fixturedef, "_is_classmethod", False) + is_staticmethod = isinstance(fixturefunc, staticmethod) + expects_self = getattr(fixturedef, "_expects_self", False) + + # Use the unified binding logic + return bind_fixture_function( + fixturefunc, + instance, + is_classmethod=is_classmethod, + is_staticmethod=is_staticmethod, + expects_self=expects_self, + ) + + +def _get_fixture_instance( + request: FixtureRequest, + fixturedef: FixtureDef[Any], +) -> Any | None: + """Get or create an instance for method fixtures. + + For function-scoped fixtures, returns the test instance if available. + For other scopes (session, module, class), creates a temporary instance + and issues a warning about the deprecated pattern. + + Returns None if no instance can be obtained. + """ + # First try to get the existing instance from the request + if hasattr(request, "instance") and request.instance is not None: + return request.instance + + # For non-function scoped fixtures that need self, we need to create a fake instance + if fixturedef.scope != "function": + # Try to find the class that owns this fixture + cls = None + + # Check if it's already a bound method + if hasattr(fixturedef.func, "__self__"): + return fixturedef.func.__self__ + + # Try to get the class from parent nodes + current = ( + request._pyfuncitem if hasattr(request, "_pyfuncitem") else request.node + ) + while current is not None: + if hasattr(current, "cls") and current.cls is not None: + cls = current.cls + break + current = current.parent + + if cls is not None: + # Create a temporary instance and warn with exact location + import warnings + + instance = cls() + + # Get the fixture's location for the warning + filename = fixturedef.func.__code__.co_filename + lineno = fixturedef.func.__code__.co_firstlineno + + from _pytest.warning_types import PytestDeprecationWarning + + warnings.warn_explicit( + f"Creating a temporary instance for {fixturedef.scope}-scoped " + f"fixture '{fixturedef.argname}' defined as a method in {cls.__name__}. " + f"This is deprecated and may lead to unexpected behavior. " + f"Consider defining the fixture as a standalone function instead of a method, " + f"or change its scope to 'function'.", + category=PytestDeprecationWarning, + filename=filename, + lineno=lineno, + module=cls.__module__ if hasattr(cls, "__module__") else None, + ) + return instance + + return None def pytest_fixture_setup( @@ -1190,7 +1468,9 @@ def pytest_fixture_setup( ) try: - result = call_fixture_func(fixturefunc, request, kwargs) + # resolve_fixture_function already handles binding for method fixtures, + # so we just call the function normally + result = call_fixture_func(fixturefunc, request, kwargs, fixturedef) except TEST_OUTCOME as e: if isinstance(e, skip.Exception): # The test requested a fixture which caused a skip. @@ -1719,6 +1999,7 @@ def _register_fixture( params: Sequence[object] | None = None, ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, autouse: bool = False, + _class_holder: type | None = None, ) -> None: """Register a fixture @@ -1803,7 +2084,7 @@ def parsefactories( holderobj = node_or_obj else: assert isinstance(node_or_obj, nodes.Node) - holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined] + holderobj = node_or_obj.obj assert isinstance(node_or_obj.nodeid, str) nodeid = node_or_obj.nodeid if holderobj in self._holderobjseen: @@ -1834,17 +2115,43 @@ def parsefactories( except AttributeError: obj = obj_ub + # Get the actual function func = obj._get_wrapped_function() - self._register_fixture( - name=fixture_name, - nodeid=nodeid, - func=func, - scope=marker.scope, - params=marker.params, - ids=marker.ids, - autouse=marker.autouse, - ) + # For plugin instances (not classes/modules), check if we need a bound method + if not safe_isclass(holderobj) and not isinstance( + holderobj, types.ModuleType + ): + # This is an instance, not a class or module + # Check if the function is an instance method (not static/class method) + is_inst_method = ( + not isinstance(func, staticmethod) + and not isinstance(func, classmethod) + and hasattr(func, "__name__") + and "." in getattr(func, "__qualname__", "") + ) + if is_inst_method: + # Create a bound method for this instance + import types as types_module + + func = types_module.MethodType(func, holderobj) + + # Store whether this is a class method fixture for later handling + fixture_kwargs = { + "name": fixture_name, + "nodeid": nodeid, + "func": func, + "scope": marker.scope, + "params": marker.params, + "ids": marker.ids, + "autouse": marker.autouse, + } + + # If this is a class, mark fixtures as class fixtures + if safe_isclass(holderobj): + fixture_kwargs["_class_holder"] = holderobj + + self._register_fixture(**fixture_kwargs) def getfixturedefs( self, argname: str, node: nodes.Node diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 6690f6ab1f8..ea12b6d9a54 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -162,6 +162,7 @@ def __init__( fspath: LEGACY_PATH | None = None, path: Path | None = None, nodeid: str | None = None, + obj: object = None, ) -> None: #: A unique name within the scope of the parent node. self.name: str = name @@ -214,6 +215,9 @@ def __init__( # Deprecated alias. Was never public. Can be removed in a few releases. self._store = self.stash + # Store the Python object (even if None) + self.obj = obj + @classmethod def from_parent(cls, parent: Node, **kw) -> Self: """Public constructor for Nodes. @@ -570,6 +574,7 @@ def __init__( config: Config | None = None, session: Session | None = None, nodeid: str | None = None, + obj: object = None, ) -> None: if path_or_parent: if isinstance(path_or_parent, Node): @@ -612,6 +617,7 @@ def __init__( session=session, nodeid=nodeid, path=path, + obj=obj, ) @classmethod diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 3f9da026799..4c4bbcbf993 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -147,6 +147,53 @@ def async_fail(nodeid: str) -> None: fail(msg, pytrace=False) +def call_with_appropriate_args( + func: Callable[..., Any], kwargs: dict[str, Any], pyfuncitem: Function +) -> Any: + """Call a function with the appropriate positional and keyword arguments. + + Handles instance methods, class methods, static methods, and regular functions + by determining what positional arguments (if any) need to be passed. + """ + # Get the parent class if we're in a class context + parent_class = pyfuncitem.parent + if not parent_class or not isinstance(parent_class, Class): + # Module-level function - no special handling needed + return func(**kwargs) + + # We're in a class context - need to determine method type + cls = parent_class.obj + method_name = pyfuncitem.originalname or pyfuncitem.name + + # Check the actual descriptor in the class __dict__ to determine type + # This is more reliable than flags that get passed around + raw_method = cls.__dict__.get(method_name) + + if isinstance(raw_method, staticmethod): + # Static method - no positional args + return func(**kwargs) + elif isinstance(raw_method, classmethod): + # Class method - may already be bound or not + if inspect.ismethod(func): + # Already bound, cls is implicit + return func(**kwargs) + else: + # Unbound, need to pass cls + return func(cls, **kwargs) + else: + # Regular instance method - needs instance + instance = ( + pyfuncitem._request.instance if hasattr(pyfuncitem, "_request") else None + ) + if instance is not None: + return func(instance, **kwargs) + else: + # No instance available but we need one + raise RuntimeError( + f"No instance available for instance method {pyfuncitem.name}" + ) + + @hookimpl(trylast=True) def pytest_pyfunc_call(pyfuncitem: Function) -> object | None: testfunction = pyfuncitem.obj @@ -154,7 +201,12 @@ def pytest_pyfunc_call(pyfuncitem: Function) -> object | None: async_fail(pyfuncitem.nodeid) funcargs = pyfuncitem.funcargs testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} - result = testfunction(**testargs) + + assert callable(testfunction) + + # Call the function with appropriate arguments + result = call_with_appropriate_args(testfunction, testargs, pyfuncitem) + if hasattr(result, "__await__") or hasattr(result, "__aiter__"): async_fail(pyfuncitem.nodeid) elif result is not None: @@ -203,7 +255,9 @@ def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool: def pytest_pycollect_makemodule(module_path: Path, parent) -> Module: - return Module.from_parent(parent, path=module_path) + # Create the Module, but delay import until collect() to preserve error handling + module: Module = Module.from_parent(parent, path=module_path) + return module @hookimpl(trylast=True) @@ -216,12 +270,23 @@ def pytest_pycollect_makeitem( if collector.istestclass(obj, name): return Class.from_parent(collector, name=name, obj=obj) elif collector.istestfunction(obj, name): - # mock seems to store unbound methods (issue473), normalize it. - obj = getattr(obj, "__func__", obj) + # For classmethods, we need the bound method from the class + # For staticmethods and regular methods, unwrap as usual + if isinstance(obj, classmethod) and isinstance(collector, Class): + # Get the bound classmethod from the class object + obj = getattr(collector.obj, name) + else: + # mock seems to store unbound methods (issue473), normalize it. + obj = getattr(obj, "__func__", obj) + # We need to try and unwrap the function if it's a functools.partial # or a functools.wrapped. # We mustn't if it's been wrapped with mock.patch (python 2 only). - if not (inspect.isfunction(obj) or inspect.isfunction(get_real_func(obj))): + if not ( + inspect.isfunction(obj) + or inspect.isfunction(get_real_func(obj)) + or inspect.ismethod(obj) + ): filename, lineno = getfslineno(obj) warnings.warn_explicit( message=PytestCollectionWarning( @@ -243,12 +308,26 @@ def pytest_pycollect_makeitem( class PyobjMixin(nodes.Node): - """this mix-in inherits from Node to carry over the typing information + """Mixin for nodes that represent Python objects. - as its intended to always mix in before a node - its position in the mro is unaffected""" + The Python object is stored in the 'obj' attribute, which is set + eagerly when the node is created (passed through the constructor). + """ _ALLOW_MARKERS = True + obj: object # Will be more specific in subclasses + + def __init__(self, *args, obj=None, **kwargs): + super().__init__(*args, **kwargs) + self.obj = obj + # Extract markers from the object if provided and markers are allowed + if obj is not None and self._ALLOW_MARKERS: + from _pytest.mark.structures import get_unpacked_marks + + marks = get_unpacked_marks(obj) + self.own_markers.extend(marks) + # Update keywords with the new marks + self.keywords.update((mark.name, mark) for mark in marks) @property def module(self): @@ -272,32 +351,6 @@ def instance(self): # Overridden by Function. return None - @property - def obj(self): - """Underlying Python object.""" - obj = getattr(self, "_obj", None) - if obj is None: - self._obj = obj = self._getobj() - # XXX evil hack - # used to avoid Function marker duplication - if self._ALLOW_MARKERS: - self.own_markers.extend(get_unpacked_marks(self.obj)) - # This assumes that `obj` is called before there is a chance - # to add custom keys to `self.keywords`, so no fear of overriding. - self.keywords.update((mark.name, mark) for mark in self.own_markers) - return obj - - @obj.setter - def obj(self, value): - self._obj = value - - def _getobj(self): - """Get the underlying Python object. May be overwritten by subclasses.""" - # TODO: Improve the type of `parent` such that assert/ignore aren't needed. - assert self.parent is not None - obj = self.parent.obj # type: ignore[attr-defined] - return getattr(obj, self.name) - def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str: """Return Python path relative to the containing module.""" parts = [] @@ -417,7 +470,10 @@ def collect(self) -> Iterable[nodes.Item | nodes.Collector]: if not collect_imported_tests and isinstance(self, Module): # Do not collect functions and classes from other modules. if inspect.isfunction(obj) or inspect.isclass(obj): - if obj.__module__ != self._getobj().__name__: + assert hasattr( + self.obj, "__name__" + ) # Module objects have __name__ + if obj.__module__ != self.obj.__name__: continue res = ihook.pytest_pycollect_makeitem( @@ -466,7 +522,9 @@ def _genfunctions(self, name: str, funcobj) -> Iterator[Function]: self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc)) if not metafunc._calls: - yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo) + yield Function.from_parent( + self, name=name, callobj=funcobj, fixtureinfo=fixtureinfo + ) else: metafunc._recompute_direct_params_indices() # Direct parametrizations taking place in module/class-specific @@ -481,6 +539,7 @@ def _genfunctions(self, name: str, funcobj) -> Iterator[Function]: yield Function.from_parent( self, name=subname, + callobj=funcobj, callspec=callspec, fixtureinfo=fixtureinfo, keywords={callspec.id: True}, @@ -547,10 +606,53 @@ def importtestmodule( class Module(nodes.File, PyCollector): """Collector for test classes and functions in a Python module.""" - def _getobj(self): - return importtestmodule(self.path, self.config) + _obj: types.ModuleType | None = None + + @property # type: ignore[override] + def obj(self) -> types.ModuleType: + """Import and return the module object, caching the result.""" + # TODO: The type: ignore above is needed because the base class PyobjMixin + # declares obj as a simple attribute, not a property. We should refactor + # the inheritance hierarchy to handle this properly. + if self._obj is None: + self._obj = importtestmodule(self.path, self.config) + # Extract markers from the module after importing + if self._ALLOW_MARKERS: + from _pytest.mark.structures import get_unpacked_marks + + marks = get_unpacked_marks(self._obj) + self.own_markers.extend(marks) + # Update keywords with the new marks + self.keywords.update((mark.name, mark) for mark in marks) + return self._obj + + @obj.setter + def obj(self, value: types.ModuleType) -> None: + """Allow setting the module object directly.""" + self._obj = value + # Extract markers when obj is set (unless it's None) + if value is not None and self._ALLOW_MARKERS: + from _pytest.mark.structures import get_unpacked_marks + + marks = get_unpacked_marks(value) + self.own_markers.extend(marks) + # Update keywords with the new marks + self.keywords.update((mark.name, mark) for mark in marks) + + @classmethod + def from_parent(cls, parent, *, path: Path | None = None, fspath=None, **kw): + """Create a Module node.""" + # Handle both path and fspath for compatibility + if path is None and fspath is not None: + path = Path(fspath) + # Don't pass obj - Module will lazy load it via property + return super().from_parent(parent=parent, path=path, **kw) def collect(self) -> Iterable[nodes.Item | nodes.Collector]: + # Access obj property to ensure module is imported + # The property will handle importing and marker extraction + _ = self.obj + self._register_setup_module_fixture() self._register_setup_function_fixture() self.session._fixturemanager.parsefactories(self) @@ -581,6 +683,7 @@ def xunit_setup_module_fixture(request) -> Generator[None]: if teardown_module is not None: _call_with_optional_argument(teardown_module, module) + assert hasattr(self.obj, "__name__") # Module objects have __name__ self.session._fixturemanager._register_fixture( # Use a unique name to speed up lookup. name=f"_xunit_setup_module_fixture_{self.obj.__name__}", @@ -617,6 +720,7 @@ def xunit_setup_function_fixture(request) -> Generator[None]: if teardown_function is not None: _call_with_optional_argument(teardown_function, function) + assert hasattr(self.obj, "__name__") # Module objects have __name__ self.session._fixturemanager._register_fixture( # Use a unique name to speed up lookup. name=f"_xunit_setup_function_fixture_{self.obj.__name__}", @@ -736,10 +840,12 @@ def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> object | N class Class(PyCollector): """Collector for test methods (and nested classes) in a Python class.""" + obj: type + @classmethod def from_parent(cls, parent, *, name, obj=None, **kw) -> Self: # type: ignore[override] """The public constructor.""" - return super().from_parent(name=name, parent=parent, **kw) + return super().from_parent(name=name, parent=parent, obj=obj, **kw) def newinstance(self): return self.obj() @@ -749,6 +855,7 @@ def collect(self) -> Iterable[nodes.Item | nodes.Collector]: return [] if hasinit(self.obj): assert self.parent is not None + assert hasattr(self.obj, "__name__") # Class objects have __name__ self.warn( PytestCollectionWarning( f"cannot collect test class {self.obj.__name__!r} because it has a " @@ -758,6 +865,7 @@ def collect(self) -> Iterable[nodes.Item | nodes.Collector]: return [] elif hasnew(self.obj): assert self.parent is not None + assert hasattr(self.obj, "__name__") # Class objects have __name__ self.warn( PytestCollectionWarning( f"cannot collect test class {self.obj.__name__!r} because it has a " @@ -769,7 +877,9 @@ def collect(self) -> Iterable[nodes.Item | nodes.Collector]: self._register_setup_class_fixture() self._register_setup_method_fixture() - self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid) + # Pass the class object directly to parsefactories + # Fixtures will be discovered from the class, not an instance + self.session._fixturemanager.parsefactories(self) return super().collect() @@ -795,6 +905,7 @@ def xunit_setup_class_fixture(request) -> Generator[None]: func = getimfunc(teardown_class) _call_with_optional_argument(func, cls) + assert hasattr(self.obj, "__qualname__") # Class objects have __qualname__ self.session._fixturemanager._register_fixture( # Use a unique name to speed up lookup. name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}", @@ -820,15 +931,22 @@ def _register_setup_method_fixture(self) -> None: def xunit_setup_method_fixture(request) -> Generator[None]: instance = request.instance + # Get the bound method instead of the unbound function method = request.function + if instance is not None and hasattr(method, "__name__"): + # Try to get the bound method from the instance + bound_method = getattr(instance, method.__name__, method) + else: + bound_method = method if setup_method is not None: func = getattr(instance, setup_name) - _call_with_optional_argument(func, method) + _call_with_optional_argument(func, bound_method) yield if teardown_method is not None: func = getattr(instance, teardown_name) - _call_with_optional_argument(func, method) + _call_with_optional_argument(func, bound_method) + assert hasattr(self.obj, "__qualname__") # Class objects have __qualname__ self.session._fixturemanager._register_fixture( # Use a unique name to speed up lookup. name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}", @@ -1125,6 +1243,8 @@ class Metafunc: test function is defined. """ + function: Callable[..., object] # The test function + def __init__( self, definition: FunctionDefinition, @@ -1390,7 +1510,7 @@ def _resolve_parameter_set_ids( ids_, self.config, nodeid=nodeid, - func_name=self.function.__name__, + func_name=getattr(self.function, "__name__", ""), ) return id_maker.make_unique_parameterset_ids() @@ -1443,7 +1563,7 @@ def _resolve_args_directness( for arg in indirect: if arg not in argnames: fail( - f"In {self.function.__name__}: indirect fixture '{arg}' doesn't exist", + f"In {getattr(self.function, '__name__', '')}: indirect fixture '{arg}' doesn't exist", pytrace=False, ) arg_directness[arg] = "indirect" @@ -1540,6 +1660,8 @@ def _ascii_escaped_by_config(val: str | bytes, config: Config | None) -> str: class Function(PyobjMixin, nodes.Item): """Item responsible for setting up and executing a Python test function. + obj: Callable[..., object] # The test function + :param name: The full function name, including any decorations like those added by parametrization (``my_func[my_param]``). @@ -1575,17 +1697,18 @@ def __init__( parent, config: Config | None = None, callspec: CallSpec2 | None = None, - callobj=NOTSET, + callobj=None, keywords: Mapping[str, Any] | None = None, session: Session | None = None, fixtureinfo: FuncFixtureInfo | None = None, originalname: str | None = None, ) -> None: - super().__init__(name, parent, config=config, session=session) + # Pass obj through to Node + super().__init__(name, parent, config=config, session=session, obj=callobj) - if callobj is not NOTSET: - self._obj = callobj - self._instance = getattr(callobj, "__self__", None) + # Function should always have an unbound callable + if callobj is None: + raise ValueError("callobj is required for Function") #: Original function name, without any decorations (for example #: parametrization adds a ``"[...]"`` suffix to function names), used to access @@ -1615,53 +1738,70 @@ def __init__( if fixtureinfo is None: fm = self.session._fixturemanager - fixtureinfo = fm.getfixtureinfo(self, self.obj, self.cls) + # For bound classmethods, don't pass cls since it's already bound in the method + # Check if this is a bound method (has __self__ attribute) + if inspect.ismethod(self.obj) and hasattr(self.obj, "__self__"): + func_for_fixtures = self.obj if callable(self.obj) else None + fixtureinfo = fm.getfixtureinfo(self, func_for_fixtures, None) + else: + func_for_fixtures = self.obj if callable(self.obj) else None # type: ignore[assignment] + fixtureinfo = fm.getfixtureinfo(self, func_for_fixtures, self.cls) self._fixtureinfo: FuncFixtureInfo = fixtureinfo self.fixturenames = fixtureinfo.names_closure self._initrequest() # todo: determine sound type limitations @classmethod - def from_parent(cls, parent, **kw) -> Self: + def from_parent(cls, parent, *, callobj=None, **kw) -> Self: """The public constructor.""" - return super().from_parent(parent=parent, **kw) + if callobj is None: + # Try to get the callable from the parent using the name + name = kw.get("name") + if name and hasattr(parent, "obj"): + callobj = getattr(parent.obj, name, None) + if callobj is None: + raise ValueError(f"Could not find callobj for {name} in {parent}") + return super().from_parent(parent=parent, callobj=callobj, **kw) def _initrequest(self) -> None: self.funcargs: dict[str, object] = {} self._request = fixtures.TopRequest(self, _ispytest=True) + # Initialize _instance attribute + self._instance: object | None = None @property - def function(self): - """Underlying python 'function' object.""" - return getimfunc(self.obj) + def instance(self) -> object | None: + """Python instance object the function is bound to. - @property - def instance(self): - try: + Returns None if not a test method, e.g. for a standalone test function. + Creates and caches an instance if this is a method in a test class. + + Even class and static methods get an instance for fixtures to use. + """ + # Check if we already have a cached instance + if self._instance is not None: return self._instance - except AttributeError: - if isinstance(self.parent, Class): - # Each Function gets a fresh class instance. - self._instance = self._getinstance() - else: - self._instance = None - return self._instance - def _getinstance(self): - if isinstance(self.parent, Class): - # Each Function gets a fresh class instance. - return self.parent.newinstance() - else: + # Check if this function is in a class + cls_node = self.getparent(Class) + if cls_node is None: + # Not in a class, so no instance return None - def _getobj(self): - instance = self.instance - if instance is not None: - parent_obj = instance - else: - assert self.parent is not None - parent_obj = self.parent.obj # type: ignore[attr-defined] - return getattr(parent_obj, self.originalname) + # All methods in a class get an instance (even class/static methods) + # This is needed for bound fixture methods + try: + self._instance = cls_node.newinstance() + except Exception: + # If we can't create an instance, return None + pass + + return self._instance + + @property + def function(self): + """Underlying python 'function' object.""" + return getimfunc(self.obj) @property def _pyfuncitem(self): @@ -1676,7 +1816,7 @@ def setup(self) -> None: self._request._fillfixtures() def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: - if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False): + if hasattr(self, "obj") and not self.config.getoption("fulltrace", False): code = _pytest._code.Code.from_function(get_real_func(self.obj)) path, firstlineno = code.path, code.firstlineno traceback = excinfo.traceback @@ -1719,6 +1859,9 @@ class FunctionDefinition(Function): """This class is a stop gap solution until we evolve to have actual function definition nodes and manage to get rid of ``metafunc``.""" + # Explicit type annotation to help mypy + obj: Callable[..., object] + def runtest(self) -> None: raise RuntimeError("function definitions are not supposed to be run as tests") diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 6ba30c4574c..a1d0d63700a 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -109,7 +109,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> tuple[bool, f"pytest_markeval_namespace() needs to return a dict, got {dictionary!r}" ) globals_.update(dictionary) - if hasattr(item, "obj"): + if hasattr(item, "obj") and hasattr(item.obj, "__globals__"): globals_.update(item.obj.__globals__) try: filename = f"<{mark.name} condition>" diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 282f7b25680..de83793173a 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -71,6 +71,7 @@ class UnitTestCase(Class): # Marker for fixturemanger.getfixtureinfo() # to declare that our children do not support funcargs. nofuncargs = True + obj: type[unittest.TestCase] # UnitTestCase always has a TestCase class def newinstance(self): # TestCase __init__ takes the method (test) name. The TestCase @@ -101,7 +102,7 @@ def collect(self) -> Iterable[Item | Collector]: x = getattr(self.obj, name) if not getattr(x, "__test__", True): continue - yield TestCaseFunction.from_parent(self, name=name) + yield TestCaseFunction.from_parent(self, name=name, callobj=x) foundsomething = True if not foundsomething: @@ -109,7 +110,9 @@ def collect(self) -> Iterable[Item | Collector]: if runtest is not None: ut = sys.modules.get("twisted.trial.unittest", None) if ut is None or runtest != ut.TestCase.runTest: - yield TestCaseFunction.from_parent(self, name="runTest") + yield TestCaseFunction.from_parent( + self, name="runTest", callobj=runtest + ) def _register_unittest_setup_class_fixture(self, cls: type) -> None: """Register an auto-use fixture to invoke setUpClass and @@ -201,10 +204,32 @@ def unittest_setup_method_fixture( class TestCaseFunction(Function): nofuncargs = True _excinfo: list[_pytest._code.ExceptionInfo[BaseException]] | None = None + _unittest_instance: unittest.TestCase | None = None + _unbound_obj: Callable[..., object] | None = None + + def __init__(self, **kwargs): + # Store the unbound method for marks + self._unbound_obj = kwargs.get("callobj") + # For unittest, pass the unbound method as obj initially + # This ensures marks from the unbound method are collected + super().__init__(**kwargs) + # After initialization, switch to using bound method as obj + if self._unbound_obj: + # Create a bound method and set it as obj + # This satisfies tests that expect obj to be a bound method + self.obj = self._getobj() + + @property + def instance(self): + """Get or create the TestCase instance for this test method.""" + if self._unittest_instance is None: + assert isinstance(self.parent, UnitTestCase) + self._unittest_instance = self.parent.obj(self.name) + return self._unittest_instance - def _getinstance(self): - assert isinstance(self.parent, UnitTestCase) - return self.parent.obj(self.name) + def _getobj(self): + """Get the bound test method.""" + return getattr(self.instance, self.name) # Backward compat for pytest-django; can be removed after pytest-django # updates + some slack. @@ -222,7 +247,7 @@ def teardown(self) -> None: self._explicit_tearDown() self._explicit_tearDown = None self._obj = None - del self._instance + self._unittest_instance = None super().teardown() def startTest(self, testcase: unittest.TestCase) -> None: @@ -342,13 +367,25 @@ def runtest(self) -> None: self._explicit_tearDown = testcase.tearDown setattr(testcase, "tearDown", lambda *args: None) - # We need to update the actual bound method with self.obj, because - # wrap_pytest_function_for_tracing replaces self.obj by a wrapper. - setattr(testcase, self.name, self.obj) + # We need to update the actual method on the instance + # wrap_pytest_function_for_tracing might have wrapped the method + # The bound method is already set on testcase, so we don't need to do anything + # unless there's been wrapping + original_func = getattr(self.parent.obj, self.name) + wrapped = False + if self._unbound_obj and self._unbound_obj != original_func: + # The function has been wrapped, need to update on testcase + import types + + bound_method = types.MethodType(self._unbound_obj, testcase) + setattr(testcase, self.name, bound_method) + wrapped = True try: testcase(result=self) finally: - delattr(testcase, self.name) + # Only delete if we added a wrapped version + if wrapped: + delattr(testcase, self.name) def _traceback_filter( self, excinfo: _pytest._code.ExceptionInfo[BaseException] diff --git a/testing/python/collect.py b/testing/python/collect.py index b26931007d9..5fa58a4d652 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -76,8 +76,10 @@ def test_syntax_error_in_module(self, pytester: Pytester) -> None: pytest.raises(modcol.CollectError, modcol.collect) def test_module_considers_pluginmanager_at_import(self, pytester: Pytester) -> None: - modcol = pytester.getmodulecol("pytest_plugins='xasdlkj',") - pytest.raises(ImportError, lambda: modcol.obj) + # The module import now happens earlier, so we catch it differently + pytester.makepyfile("pytest_plugins='xasdlkj',") + result = pytester.runpytest() + result.stdout.fnmatch_lines(["*ImportError*"]) def test_invalid_test_module_name(self, pytester: Pytester) -> None: a = pytester.mkdir("a") diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 8b97d35c21e..f52c04c645e 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1134,6 +1134,9 @@ def test_1(arg): reprec.assertoutcome(passed=2) +@pytest.mark.filterwarnings( + "ignore:Creating a temporary instance.*:pytest.PytestDeprecationWarning" +) class TestRequestSessionScoped: @pytest.fixture(scope="session") def session_request(self, request): @@ -1748,13 +1751,18 @@ class TestClass(object): def hello(self, request): return "class" def test_hello(self, item, fm): + from _pytest.fixtures import resolve_fixture_function faclist = fm.getfixturedefs("hello", item) print(faclist) assert len(faclist) == 3 assert faclist[0].func(item._request) == "conftest" assert faclist[1].func(item._request) == "module" - assert faclist[2].func(item._request) == "class" + + # For class method fixtures, resolve_fixture_function returns a bound method + resolved_func = resolve_fixture_function(faclist[2], item._request) + # The bound method only needs the request argument since self is already bound + assert resolved_func(item._request) == "class" """ ) reprec = pytester.inline_run("-s") diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 20ccacf4b73..37d7bc57eca 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1,6 +1,7 @@ # mypy: allow-untyped-defs from __future__ import annotations +from collections.abc import Callable from collections.abc import Iterator from collections.abc import Sequence import dataclasses @@ -46,10 +47,19 @@ class FixtureManagerMock: class SessionMock: _fixturemanager: FixtureManagerMock - @dataclasses.dataclass class DefinitionMock(python.FunctionDefinition): _nodeid: str - obj: object + obj: Callable[..., object] + + def __init__( + self, + *, + _nodeid: str, + obj: Callable[..., object], + **kwargs, + ): + self._nodeid = _nodeid + self.obj = obj names = getfuncargnames(func) fixtureinfo: Any = FuncFixtureInfoMock(names) diff --git a/testing/test_collection.py b/testing/test_collection.py index bb2fd82c898..ebffe19af54 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1610,8 +1610,8 @@ def test_class_from_parent(request: FixtureRequest) -> None: """Ensure Class.from_parent can forward custom arguments to the constructor.""" class MyCollector(pytest.Class): - def __init__(self, name, parent, x): - super().__init__(name, parent) + def __init__(self, name, parent, x, **kwargs): + super().__init__(name, parent, **kwargs) self.x = x @classmethod diff --git a/testing/test_conftest.py b/testing/test_conftest.py index bd083574ffc..1652eccbbed 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -41,6 +41,9 @@ def conftest_setinitial( @pytest.mark.usefixtures("_sys_snapshot") +@pytest.mark.filterwarnings( + "ignore:Creating a temporary instance.*:pytest.PytestDeprecationWarning" +) class TestConftestValueAccessGlobal: @pytest.fixture(scope="module", params=["global", "inpackage"]) def basedir(self, request, tmp_path_factory: TempPathFactory) -> Generator[Path]: diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py index 80936667835..87d0f6388c0 100644 --- a/testing/test_legacypath.py +++ b/testing/test_legacypath.py @@ -100,6 +100,9 @@ def test_fixturerequest_getmodulepath(pytester: pytest.Pytester) -> None: assert req.fspath == modcol.fspath # type: ignore[attr-defined] +@pytest.mark.filterwarnings( + "ignore:Creating a temporary instance.*:pytest.PytestDeprecationWarning" +) class TestFixtureRequestSessionScoped: @pytest.fixture(scope="session") def session_request(self, request): diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 0880c355557..4b74ac1f6c9 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -120,6 +120,9 @@ def ns_param(request: pytest.FixtureRequest) -> bool: return bool(request.param) +@pytest.mark.filterwarnings( + "ignore:Creating a temporary instance.*:pytest.PytestDeprecationWarning" +) class TestImportPath: """ @@ -128,6 +131,9 @@ class TestImportPath: Having our own pyimport-like function is inline with removing py.path dependency in the future. """ + # TODO: The path1 fixture is session-scoped but defined as a method, which requires + # creating a temporary instance. This should be refactored to be a standalone + # session-scoped fixture instead of a method. @pytest.fixture(scope="session") def path1(self, tmp_path_factory: TempPathFactory) -> Generator[Path]: path = tmp_path_factory.mktemp("path") diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000000..5834d477a3a --- /dev/null +++ b/uv.lock @@ -0,0 +1,334 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" + +[[package]] +name = "argcomplete" +version = "3.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/0f/861e168fc813c56a78b35f3c30d91c6757d1fd185af1110f1aec784b35d0/argcomplete-3.6.2.tar.gz", hash = "sha256:d0519b1bc867f5f4f4713c41ad0aba73a4a5f007449716b16f385f2166dc6adf", size = 73403, upload-time = "2025-04-03T04:57:03.52Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/da/e42d7a9d8dd33fa775f467e4028a47936da2f01e4b0e561f9ba0d74cb0ca/argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591", size = 43708, upload-time = "2025-04-03T04:57:01.591Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "certifi" +version = "2025.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[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, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "elementpath" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/f0/a6b7549313460c88c1371fd6275358964e16302e889d22e0d7ce89903027/elementpath-5.0.2.tar.gz", hash = "sha256:26224a33ad9edc82bfa5b26a767a640c8407fbbf9e019b1c369f718dd21823fb", size = 364400, upload-time = "2025-06-18T07:53:04.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/db/588b8974b93e708e98b986dc2895a4901db5653ea49ecb511a2bcb3da77b/elementpath-5.0.2-py3-none-any.whl", hash = "sha256:d1e9c23e22c266bc83e2e456653087b3197e83508b8325421c7ca80e2ef37259", size = 245467, upload-time = "2025-06-18T07:53:02.597Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "hypothesis" +version = "6.135.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/a5/d4f74ba61bbe5dd001c998ae8b85f9bfdc6cd29e6c5693d1116847b64251/hypothesis-6.135.14.tar.gz", hash = "sha256:2666df50b3cc40ea08b161a5389d6a1cd5aa3cab0dd8fde0ae339389714a4f67", size = 452884, upload-time = "2025-06-20T19:16:38.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/cf/491a487229b04a2ad56175c74700cfb79635dfce2d942becc6ab10c0ceb9/hypothesis-6.135.14-py3-none-any.whl", hash = "sha256:0dd5b8095e36bd288367c631f864a16c30500b01b17943dcea681233f7421860", size = 519115, upload-time = "2025-06-20T19:16:34.539Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "mock" +version = "5.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/07/8c/14c2ae915e5f9dca5a22edd68b35be94400719ccfa068a03e0fb63d0f6f6/mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0", size = 92796, upload-time = "2025-03-03T12:31:42.911Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/d9/617e6af809bf3a1d468e0d58c3997b1dc219a9a9202e650d30c2fc85d481/mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f", size = 31617, upload-time = "2025-03-03T12:31:41.518Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +source = { editable = "." } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] + +[package.optional-dependencies] +dev = [ + { name = "argcomplete" }, + { name = "attrs" }, + { name = "hypothesis" }, + { name = "mock" }, + { name = "requests" }, + { name = "setuptools" }, + { name = "xmlschema" }, +] + +[package.metadata] +requires-dist = [ + { name = "argcomplete", marker = "extra == 'dev'" }, + { name = "attrs", marker = "extra == 'dev'", specifier = ">=19.2" }, + { name = "colorama", marker = "sys_platform == 'win32'", specifier = ">=0.4" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'", specifier = ">=1" }, + { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=3.56" }, + { name = "iniconfig", specifier = ">=1" }, + { name = "mock", marker = "extra == 'dev'" }, + { name = "packaging", specifier = ">=20" }, + { name = "pluggy", specifier = ">=1.5,<2" }, + { name = "pygments", specifier = ">=2.7.2" }, + { name = "requests", marker = "extra == 'dev'" }, + { name = "setuptools", marker = "extra == 'dev'" }, + { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=1" }, + { name = "xmlschema", marker = "extra == 'dev'" }, +] +provides-extras = ["dev"] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" }, +] + +[[package]] +name = "xmlschema" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "elementpath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/cb/dc0236ae209c924c1a7f4d3ae9d3371980beda85d1e0b5bd2d4f07372e48/xmlschema-4.1.0.tar.gz", hash = "sha256:88ac771cf94d5fc6bbd1a763db8c157f3d683ad23120b0d0b8c46fe4537f2adf", size = 633811, upload-time = "2025-06-05T21:17:39.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/2c/3ba8f8aeb3cb566e88a61ab0b0b767d5398d1e7293a0f7a76df9051e2b08/xmlschema-4.1.0-py3-none-any.whl", hash = "sha256:eabf610f398a58700bc4ac94380ad9ce558297a3f9ca8b7722ed3f7888eb4498", size = 458466, upload-time = "2025-06-05T21:17:35.265Z" }, +]