From e1924830e7fb5cf52bf94559746dfc3bbdfc6dbb Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Fri, 2 May 2025 16:36:27 +0200 Subject: [PATCH 01/40] feat(environment): introduce structured environment declaration and validation via Environment model --- src/importspy/models.py | 13 +++++- src/importspy/utilities/system_util.py | 20 ++++++--- src/importspy/validators/system_validator.py | 42 ++++++++++++++----- .../validators/variable_validator.py | 4 +- tests/validators/test_system_validator.py | 15 +++++-- 5 files changed, 68 insertions(+), 26 deletions(-) diff --git a/src/importspy/models.py b/src/importspy/models.py index e6c3651..d1ca56e 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -56,6 +56,13 @@ def validate_interpreter(cls, value: str): raise ValueError(Errors.INVALID_PYTHON_INTERPRETER.format(Constants.SUPPORTED_PYTHON_IMPLEMENTATION, value)) return value +class Environment(BaseModel): + """ + Represents a set of environment variables and secret keys + defined for the system or application runtime. + """ + variables: Optional[List['Variable']] = None + secrets: Optional[List[str]] = None class System(BaseModel): """ @@ -63,7 +70,7 @@ class System(BaseModel): and Python runtimes configured within the system. """ os: str - envs: Optional[dict] = Field(default=None, repr=False) + environment: Optional[Environment] = None pythons: List[Python] @field_validator('os') @@ -284,7 +291,9 @@ def from_module(cls, info_module: ModuleType): systems=[ System( os=os, - envs=envs, + environment=Environment( + variables=envs + ), pythons=[ Python( version=python_version, diff --git a/src/importspy/utilities/system_util.py b/src/importspy/utilities/system_util.py index a886158..415234f 100644 --- a/src/importspy/utilities/system_util.py +++ b/src/importspy/utilities/system_util.py @@ -26,10 +26,14 @@ import os import logging import platform +from collections import namedtuple +from typing import List logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) +VariableInfo = namedtuple('VariableInfo', ["name", "value"]) + class SystemUtil: """ System-level utility class for environment inspection. @@ -63,18 +67,22 @@ def extract_os(self) -> str: """ return platform.system().lower() - def extract_envs(self) -> dict: + def extract_envs(self) -> List[VariableInfo]: """ - Retrieve all current environment variables. + Return a list of environment variables available in the current process. + + Uses `os.environ.items()` to collect all key-value pairs. Returns ------- - dict - Dictionary of key-value environment variables. + List[VariableInfo] + + A list of VariableInfo instances, each representing an environment variable. Example ------- >>> SystemUtil().extract_envs() - {'PATH': '/usr/bin:/bin', 'HOME': '/home/user', ...} + [VariableInfo(name='PATH', value='/usr/bin'), VariableInfo(name='HOME', value='/home/user'), ...] """ - return dict(os.environ) + return [VariableInfo(name, value) for name, value in os.environ.items()] + diff --git a/src/importspy/validators/system_validator.py b/src/importspy/validators/system_validator.py index 4bbb42c..139fe0b 100644 --- a/src/importspy/validators/system_validator.py +++ b/src/importspy/validators/system_validator.py @@ -14,10 +14,13 @@ """ from typing import List -from ..models import System +from ..models import ( + System, + Environment +) from ..errors import Errors -from .common_validator import CommonValidator from .python_validator import PythonValidator +from .variable_validator import VariableValidator class SystemValidator: @@ -40,6 +43,7 @@ def __init__(self): Initialize the system validator and prepare supporting validators. """ self._python_validator = PythonValidator() + self._environment_validator = SystemValidator.EnvironmentValidator() def validate( self, @@ -81,19 +85,35 @@ def validate( if not systems_2: raise ValueError(Errors.ELEMENT_MISSING.format(systems_1)) - - cv = CommonValidator() + system_2 = systems_2[0] for system_1 in systems_1: if system_1.os == system_2.os: - if system_1.envs: - cv.dict_validate( - system_1.envs, - system_2.envs, - Errors.ENV_VAR_MISSING, - Errors.ENV_VAR_MISMATCH - ) + if system_1.environment: + self._environment_validator.validate(system_1.environment, system_2.environment) if system_1.pythons: self._python_validator.validate(system_1.pythons, system_2.pythons) return + + class EnvironmentValidator: + + def __init__(self): + self._variable_validator = VariableValidator() + + def validate(self, + environment_1:Environment, + environment_2: Environment): + + if not environment_1: + return + + if not environment_2: + raise ValueError(Errors.ELEMENT_MISSING.format(environment_1)) + + variables_2 = environment_2.variables + + if environment_1.variables: + variables_1 = environment_1.variables + self._variable_validator.validate(variables_1, variables_2) + return \ No newline at end of file diff --git a/src/importspy/validators/variable_validator.py b/src/importspy/validators/variable_validator.py index a3fdcd5..2e7a437 100644 --- a/src/importspy/validators/variable_validator.py +++ b/src/importspy/validators/variable_validator.py @@ -88,6 +88,4 @@ def validate( vars_1.value, vars_2.value ) - ) - - return True \ No newline at end of file + ) \ No newline at end of file diff --git a/tests/validators/test_system_validator.py b/tests/validators/test_system_validator.py index feb79f3..5098344 100644 --- a/tests/validators/test_system_validator.py +++ b/tests/validators/test_system_validator.py @@ -1,6 +1,8 @@ import pytest from importspy.models import ( - System + System, + Environment, + Variable ) from importspy.config import Config from importspy.constants import Constants @@ -17,7 +19,12 @@ class TestSystemValidator: def data_1(self): return [System( os=Config.OS_LINUX, - envs={"CI": "true"}, + environment=Environment( + variables=[Variable( + name="CI", + value="true" + )] + ), pythons=[] )] @@ -34,7 +41,7 @@ def os_windows_setter(self, data_1:List[System]): @pytest.fixture def envs_setter(self, data_2): - data_2[0].envs = {"CI": "true"} + data_2[0].environment = Environment(variables=[(Variable(name="CI", value="true"))]) @pytest.mark.usefixtures("envs_setter") def test_system_os_match(self, data_1:List[System], data_2:List[System]): @@ -55,7 +62,7 @@ def test_system_os_mismatch(self, data_1:List[System], data_2:List[System]): with pytest.raises( ValueError, match=re.escape( - Errors.ENV_VAR_MISSING.format(data_1[0].envs) + Errors.ELEMENT_MISSING.format(data_1[0].environment) ) ): self.validator.validate(data_1, data_2) From f242d6423f04d47309060e46cafa6162128f272f Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Fri, 2 May 2025 22:59:56 +0200 Subject: [PATCH 02/40] refactor(errors): unify error messages under consistent templates - Introduced a structured error model using three generic templates: - ELEMENT_MISSING for expected but missing elements - ELEMENT_MISMATCH for elements present but not matching the contract - ELEMENT_INVALID for values outside the allowed domain - Refactored all error messages in `errors.py` to derive from these templates - Added Scope constants to centralize context classification for variables, attributes, arguments - Removed redundant or duplicated error strings already covered by scoped mappings - Updated validators to pass scope and format context-aware error messages consistently - Removed deprecated `dict_validate` method and return statements no longer used - Cleaned up debug logging with explicit scope awareness in `VariableValidator` This refactor ensures uniformity, better error readability, and easier future maintenance. --- src/importspy/constants.py | 9 + src/importspy/errors.py | 228 ++++++++---------- src/importspy/validators/common_validator.py | 64 +---- .../validators/function_validator.py | 2 - .../validators/variable_validator.py | 12 +- 5 files changed, 123 insertions(+), 192 deletions(-) diff --git a/src/importspy/constants.py b/src/importspy/constants.py index 6bf93ee..6036a5d 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -129,3 +129,12 @@ class Constants: "[Operation: {operation}] [Status: {status}] " "[Details: {details}]" ) + +class Scope: + + SCOPE_VARIABLE = "variable" + SCOPE_ENVIRONMENT = "environment" + SCOPE_FUNCTION_ARG = "function_arg" + SCOPE_METHOD_ARG_IN_CLASS = "method_arg_in_class" + SCOPE_CLASS_ATTRIBUTE = "class_attribute" + SCOPE_INSTANCE_ATTRIBUTE = "instance_attribute" diff --git a/src/importspy/errors.py b/src/importspy/errors.py index e5b0562..a60ad3a 100644 --- a/src/importspy/errors.py +++ b/src/importspy/errors.py @@ -1,139 +1,123 @@ -class Errors: +from .constants import Scope +class Errors: """ Central repository for error messages used in ImportSpy’s validation engine. - This class contains formatted string constants for every type of structural, - semantic, and runtime validation error that can be raised during contract - evaluation. These error messages provide actionable feedback and are used - throughout ImportSpy's exception handling system. - - The format strings typically include placeholders for contextual details, - such as expected and actual values, function names, class names, or - annotation types. Grouped by category, these constants help keep the - validation engine consistent and maintainable. - - Attributes: - ANALYSIS_RECURSION_WARNING (str): - General warning when the validation process detects recursive self-analysis. - - FILENAME_MISMATCH (str): - Raised when the module filename does not match the expected contract. - - VERSION_MISMATCH (str): - Triggered when the module version deviates from the one declared in the contract. - - ENV_VAR_MISSING (str): - Raised when a required environment variable is not found in the system. - - ENV_VAR_MISMATCH (str): - Indicates a mismatch between the expected and actual values of an environment variable. - - VAR_MISSING (str): - Raised when a required variable is not present in the importing module. - - VAR_MISMATCH (str): - Raised when a variable is present but its value does not match what the contract expects. - - FUNCTIONS_MISSING (str): - Used when one or more expected functions are missing from the module. - - FUNCTION_RETURN_ANNOTATION_MISMATCH (str): - Indicates a mismatch in the return type annotation of a function. - - VARIABLE_MISMATCH (str): - Raised when a declared variable's value does not match the expected value. - - VARIABLE_MISSING (str): - Raised when a declared variable is not found. - - ARGUMENT_MISMATCH (str): - Raised when a function argument has an unexpected name or annotation. - - ARGUMENT_MISSING (str): - Raised when a required argument is missing in the function signature. - - CLASS_MISSING (str): - Triggered when a required class is not defined in the importing module. - - CLASS_ATTRIBUTE_MISSING (str): - Raised when an expected attribute is not found in a class definition. - - CLASS_ATTRIBUTE_MISMATCH (str): - Raised when an attribute exists but its value does not match what the contract expects. - - CLASS_SUPERCLASS_MISSING (str): - Triggered when a required superclass is missing from a class declaration. - - INVALID_ATTRIBUTE_TYPE (str): - Raised when an attribute has an unsupported type. - - INVALID_ARCHITECTURE (str): - Triggered when the system architecture does not match any of the allowed values. - - INVALID_OS (str): - Triggered when the operating system is not among those supported. - - INVALID_PYTHON_VERSION (str): - Raised when the current Python version is not one of the accepted versions. - - INVALID_PYTHON_INTERPRETER (str): - Raised when the Python interpreter is not among the supported ones. - - INVALID_ANNOTATION (str): - Raised when a variable, argument, or return annotation is unsupported. - - ELEMENT_MISSING (str): - Generic error for any expected element missing from the system or module context. + All validation errors extend one of three generic templates: + - ELEMENT_MISSING: expected but not found + - ELEMENT_MISMATCH: found but does not match the declared contract + - ELEMENT_INVALID: found but not allowed (invalid value from limited set) """ - + # General Warnings ANALYSIS_RECURSION_WARNING = ( "Warning: Analysis recursion detected. Avoid analyzing code that itself handles analysis, " "to prevent stack overflow or performance issues." ) + # Generic Templates + ELEMENT_MISSING = ( + "{0} is declared but missing in the system. " + "Ensure it is properly defined and implemented." + ) + + ELEMENT_MISMATCH = ( + "{0} is defined but its value does not match the expected one. " + "Expected: {1!r}, Found: {2!r}. " + "Check the implementation and update the contract or the code accordingly." + ) + + ELEMENT_INVALID = ( + "{0} has an invalid value. " + "Allowed values: {1}. Found: {2!r}. " + "Update the environment or contract accordingly." + ) + # Module Validation Errors - FILENAME_MISMATCH = "Filename mismatch: expected '{0}', found '{1}'." - VERSION_MISMATCH = "Version mismatch: expected '{0}', found '{1}'." - ENV_VAR_MISSING = "Missing environment variable: '{0}'. Ensure it is defined in the system." - ENV_VAR_MISMATCH = "Environment variable value mismatch: expected '{0}', found '{1}'." - VAR_MISSING = "Missing variable: '{0}'. Ensure it is defined." - VAR_MISMATCH = "Variable value mismatch: expected '{0}', found '{1}'." - FUNCTIONS_MISSING = "Missing {0}: '{1}'. Ensure it is defined." + FILENAME_MISMATCH = ELEMENT_MISMATCH.format( + "The module filename", "{0}", "{1}" + ) + VERSION_MISMATCH = ELEMENT_MISMATCH.format( + "The module version", "{0}", "{1}" + ) - # Function and Class Validation Errors - FUNCTION_RETURN_ANNOTATION_MISMATCH = ( - "Return annotation mismatch for {0} '{1}': expected '{2}', found '{3}'." - ) - VARIABLE_MISMATCH = "Variable mismatch'{1}': expected '{2}', found '{3}'." - VARIABLE_MISSING = "Missing variable '{0}'" - ARGUMENT_MISMATCH = "Argument mismatch for {0} '{1}': expected '{2}', found '{3}'." - ARGUMENT_MISSING = "Missing argument '{0}' in {1}." - - CLASS_MISSING = "Missing class: '{0}'. Ensure it is defined." - CLASS_ATTRIBUTE_MISSING = "Missing attribute '{0}' in class '{1}'." - CLASS_ATTRIBUTE_MISMATCH = ( - "Attribute value mismatch for '{0}' in class '{1}': expected '{2}', found '{3}'." - ) - CLASS_SUPERCLASS_MISSING = ( - "Missing superclass '{0}' in class '{1}'. Ensure that '{1}' extends '{0}'." - ) - INVALID_ATTRIBUTE_TYPE = "Invalid attribute type: '{0}'. Supported types are: {1}." + # Function and Class Validation + FUNCTION_RETURN_ANNOTATION_MISMATCH = ELEMENT_MISMATCH.format( + "The return annotation of {0} '{1}'", "{2}", "{3}" + ) - # Runtime Validation Errors - INVALID_ARCHITECTURE = "Invalid architecture: expected '{0}', found '{1}'." - INVALID_OS = "Invalid Operating System: expected one of {0}, but found '{1}'." + CLASS_MISSING = ELEMENT_MISSING.format('The class "{0}"') + CLASS_SUPERCLASS_MISSING = ELEMENT_MISSING.format( + 'The superclass "{0}" in class "{1}"' + ) - # Python Valitation Errors - INVALID_PYTHON_VERSION = "Invalid python version: expected one of '{0}', but found '{1}'." - INVALID_PYTHON_INTERPRETER = "Invalid python interpreter: expected one of '{0}', but found '{1}'." + # Annotation and Runtime Validation + INVALID_ANNOTATION = ELEMENT_INVALID.format( + "The annotation", "{allowed}", "{found}" + ) + INVALID_ATTRIBUTE_TYPE = ELEMENT_INVALID.format( + "The attribute type", "{allowed}", "{found}" + ) + INVALID_ARCHITECTURE = ELEMENT_INVALID.format( + "The system architecture", "{allowed}", "{found}" + ) + INVALID_OS = ELEMENT_INVALID.format( + "The operating system", "{allowed}", "{found}" + ) + INVALID_PYTHON_VERSION = ELEMENT_INVALID.format( + "The Python version", "{allowed}", "{found}" + ) + INVALID_PYTHON_INTERPRETER = ELEMENT_INVALID.format( + "The Python interpreter", "{allowed}", "{found}" + ) - # Annotation Validation - INVALID_ANNOTATION = "Invalid annotation: expected one of {0}, but found '{1}'." + # Scoped MISSING Errors + VARIABLE_MISSING_ERROR = ELEMENT_MISSING.format('The variable "{name}"') + ENV_VAR_MISSING_ERROR = ELEMENT_MISSING.format('The environment variable "{name}"') + FUNCTION_ARG_MISSING_ERROR = ELEMENT_MISSING.format('The argument "{name}" of function "{function}"') + METHOD_ARG_IN_CLASS_MISSING_ERROR = ELEMENT_MISSING.format( + 'The argument "{name}" of method "{method}" of class "{class_name}"' + ) + CLASS_ATTRIBUTE_MISSING_ERROR = ELEMENT_MISSING.format( + 'The class attribute "{name}" of class "{class_name}"' + ) + INSTANCE_ATTRIBUTE_MISSING_ERROR = ELEMENT_MISSING.format( + 'The instance attribute "{name}" of class "{class_name}"' + ) - # Generic Element Missing - ELEMENT_MISSING = ( - "{0} is declared but missing in the system. " - "Ensure it is properly defined and implemented." - ) \ No newline at end of file + # Scoped MISMATCH Errors + VARIABLE_MISMATCH_ERROR = ELEMENT_MISMATCH.format('The variable "{name}"', "{expected}", "{actual}") + ENV_VAR_MISMATCH_ERROR = ELEMENT_MISMATCH.format('The environment variable "{name}"', "{expected}", "{actual}") + FUNCTION_ARG_MISMATCH_ERROR = ELEMENT_MISMATCH.format( + 'The argument "{name}" of function "{function}"', "{expected}", "{actual}" + ) + METHOD_ARG_IN_CLASS_MISMATCH_ERROR = ELEMENT_MISMATCH.format( + 'The argument "{name}" of method "{method}" of class "{class_name}"', + "{expected}", "{actual}" + ) + CLASS_ATTRIBUTE_MISMATCH_ERROR = ELEMENT_MISMATCH.format( + 'The class attribute "{name}" of class "{class_name}"', "{expected}", "{actual}" + ) + INSTANCE_ATTRIBUTE_MISMATCH_ERROR = ELEMENT_MISMATCH.format( + 'The instance attribute "{name}" of class "{class_name}"', "{expected}", "{actual}" + ) + + # Scope-to-message mapping + SCOPE_ELEMENT_MISSING_ERRORS = { + Scope.SCOPE_VARIABLE: VARIABLE_MISSING_ERROR, + Scope.SCOPE_ENVIRONMENT: ENV_VAR_MISSING_ERROR, + Scope.SCOPE_FUNCTION_ARG: FUNCTION_ARG_MISSING_ERROR, + Scope.SCOPE_METHOD_ARG_IN_CLASS: METHOD_ARG_IN_CLASS_MISSING_ERROR, + Scope.SCOPE_CLASS_ATTRIBUTE: CLASS_ATTRIBUTE_MISSING_ERROR, + Scope.SCOPE_INSTANCE_ATTRIBUTE: INSTANCE_ATTRIBUTE_MISSING_ERROR, + } + + SCOPE_ELEMENT_MISMATCH_ERRORS = { + Scope.SCOPE_VARIABLE: VARIABLE_MISMATCH_ERROR, + Scope.SCOPE_ENVIRONMENT: ENV_VAR_MISMATCH_ERROR, + Scope.SCOPE_FUNCTION_ARG: FUNCTION_ARG_MISMATCH_ERROR, + Scope.SCOPE_METHOD_ARG_IN_CLASS: METHOD_ARG_IN_CLASS_MISMATCH_ERROR, + Scope.SCOPE_CLASS_ATTRIBUTE: CLASS_ATTRIBUTE_MISMATCH_ERROR, + Scope.SCOPE_INSTANCE_ATTRIBUTE: INSTANCE_ATTRIBUTE_MISMATCH_ERROR, + } diff --git a/src/importspy/validators/common_validator.py b/src/importspy/validators/common_validator.py index 27227af..4b569ee 100644 --- a/src/importspy/validators/common_validator.py +++ b/src/importspy/validators/common_validator.py @@ -96,66 +96,4 @@ def list_validate( for expected_element in list1: if expected_element not in list2: - raise ValueError(missing_error.format(expected_element, *args)) - - def dict_validate( - self, - dict1: Dict, - dict2: Dict, - missing_error: str, - mismatch_error: str - ) -> bool: - """ - Validates keys and values between two dictionaries. - - Parameters - ---------- - dict1 : Dict - The expected key-value mapping. - dict2 : Dict - The actual dictionary to check. - missing_error : str - Format string for a missing key (e.g., "Key missing: {0}"). - mismatch_error : str - Format string for value mismatch (e.g., "Mismatch for {0}: {1} != {2}"). - - Returns - ------- - bool - True if validation passes. - - Raises - ------ - ValueError - If a key is missing or a value does not match. - - Example - ------- - >>> CommonValidator().dict_validate( - ... {"x": 1}, {"x": 2}, - ... "Missing key: {0}", - ... "Mismatch for {0}: expected {1}, got {2}" - ... ) - """ - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Dict validating", - status="Starting", - details=f"Expected dict: {dict1} ; Current dict: {dict2}" - ) - ) - - if not dict1: - return True - if not dict2: - raise ValueError(missing_error.format(dict1)) - - for expected_key, expected_value in dict1.items(): - if expected_key in dict2: - actual_value = dict2[expected_key] - if expected_value != actual_value: - raise ValueError(mismatch_error.format(expected_key, expected_value, actual_value)) - else: - raise ValueError(missing_error.format(expected_key)) - - return True + raise ValueError(missing_error.format(expected_element, *args)) \ No newline at end of file diff --git a/src/importspy/validators/function_validator.py b/src/importspy/validators/function_validator.py index 72ef6b7..6cb63b4 100644 --- a/src/importspy/validators/function_validator.py +++ b/src/importspy/validators/function_validator.py @@ -76,7 +76,6 @@ def validate( ------- >>> validator = FunctionValidator() >>> validator.validate(expected_functions, actual_functions, classname="MyService") - True """ context_name = f"method in class {classname}" if classname else "function" @@ -157,4 +156,3 @@ def validate( details="Validation successful." ) ) - return True diff --git a/src/importspy/validators/variable_validator.py b/src/importspy/validators/variable_validator.py index 2e7a437..1d3281d 100644 --- a/src/importspy/validators/variable_validator.py +++ b/src/importspy/validators/variable_validator.py @@ -6,7 +6,8 @@ class VariableValidator: - def __init__(self): + def __init__(self, scope: str): + self.scope = scope self.logger = LogManager().get_logger(self.__class__.__name__) def validate( @@ -14,12 +15,13 @@ def validate( variables_1: List[Variable], variables_2: List[Variable], ): + self.logger.debug(f"Scope of valitadion: {self.scope}") self.logger.debug(f"Type of variables_1: {type(variables_1)}") self.logger.debug( Constants.LOG_MESSAGE_TEMPLATE.format( operation="Variable validating", status="Starting", - details=f"Expected Variables: {variables_1} ; Current Variables: {variables_2}" + details=f"Expected Variables: {variables_1} in {self.scope} scope ; Current Variables: {variables_2} in {self.scope} scope" ) ) @@ -28,7 +30,7 @@ def validate( Constants.LOG_MESSAGE_TEMPLATE.format( operation="Check if variables_1 is not none", status="Finished", - details=f"No expected Variables; variables_1: {variables_1}" + details=f"No expected Variables in {self.scope} scope; variables_1: {variables_1} in {self.scope} scope" ) ) return @@ -38,10 +40,10 @@ def validate( Constants.LOG_MESSAGE_TEMPLATE.format( operation="Checking variables_2 when variables_1 is missing", status="Finished", - details=f"No actual Variables found; variables_2: {variables_2}" + details=f"No actual Variables in found in {self.scope} scope; variables_2: {variables_2} in {self.scope} scope" ) ) - raise ValueError(Errors.ELEMENT_MISSING.format(variables_1)) + raise ValueError(Errors.ELEMENT_MISSING.format(f"{variables_1}")) for vars_1 in variables_1: self.logger.debug( From 2abb3e6a78b50bbfb2c548382602a9fbcdfde14b Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Sat, 3 May 2025 14:45:10 +0200 Subject: [PATCH 03/40] refactor(errors,validators): remove legacy scope-based error maps and unify context handling - Removed deprecated Scope-to-error mappings from errors.py in favor of context-based formatting - Deleted legacy validators: ArgumentValidator and AttributeValidator - Replaced them with a unified Context-based VariableValidator supporting different scopes - Simplified Errors class to retain only reusable base templates and a few module-specific messages - Introduced context-aware error rendering in variable_validator.py via Context subclasses - Updated FunctionValidator and SystemValidator to pass proper Context instances This refactoring improves modularity, eliminates redundancy, and prepares the validation engine for dynamic, pluggable scope contexts. --- src/importspy/constants.py | 3 - src/importspy/contexts.py | 202 ++++++++++++++++++ src/importspy/errors.py | 88 ++------ .../validators/argument_validator.py | 162 -------------- .../validators/attribute_validator.py | 172 --------------- .../validators/function_validator.py | 76 +------ src/importspy/validators/system_validator.py | 5 +- .../validators/variable_validator.py | 110 ++++++---- 8 files changed, 310 insertions(+), 508 deletions(-) create mode 100644 src/importspy/contexts.py delete mode 100644 src/importspy/validators/argument_validator.py delete mode 100644 src/importspy/validators/attribute_validator.py diff --git a/src/importspy/constants.py b/src/importspy/constants.py index 6036a5d..98bdd86 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -130,11 +130,8 @@ class Constants: "[Details: {details}]" ) -class Scope: - SCOPE_VARIABLE = "variable" SCOPE_ENVIRONMENT = "environment" SCOPE_FUNCTION_ARG = "function_arg" SCOPE_METHOD_ARG_IN_CLASS = "method_arg_in_class" SCOPE_CLASS_ATTRIBUTE = "class_attribute" - SCOPE_INSTANCE_ATTRIBUTE = "instance_attribute" diff --git a/src/importspy/contexts.py b/src/importspy/contexts.py new file mode 100644 index 0000000..a3ae0da --- /dev/null +++ b/src/importspy/contexts.py @@ -0,0 +1,202 @@ +""" +importspy.context +================= + +This module defines context-aware validation classes used in ImportSpy +to generate precise and meaningful error messages during contract enforcement. + +Each context class encapsulates the structural role of an element +(such as a variable, function argument, or class attribute) and provides +methods to render human-readable labels and formatted error strings. + +By localizing the rendering logic and scope-specific semantics, +these classes ensure clarity, traceability, and consistency in validation reports. + +These context-aware abstractions are essential to separate structural expectations +from dynamic enforcement, giving ImportSpy the ability to issue expressive, +actionable diagnostics across scopes and validation levels. +""" + +from typing import Union, Sequence +from .errors import Errors +from .constants import Constants + + +class Context: + """ + Abstract base class for all validation contexts in ImportSpy. + + A context defines the semantic and structural scope of an element under validation. + It provides interface methods for generating context-aware error messages + related to missing, mismatched, or invalid elements. + """ + + def __init__(self, scope: str): + """ + Parameters + ---------- + scope : str + A Scope constant string that identifies the validation context type. + """ + self.scope = scope + + +class SimpleVariableContext(Context): + """ + Context for variables without type annotations. + + Typically used for simple key-value definitions such as environment variables, + module-level constants, or untyped configuration fields. + """ + + def __init__(self): + super().__init__(Constants.SCOPE_ENVIRONMENT) + + def render_label(self, name: str) -> str: + """ + Create a human-readable label for the variable. + + Parameters + ---------- + name : str + The name of the variable. + + Returns + ------- + str + A formatted string like 'The environment variable "FOO"'. + """ + return f'The environment variable "{name}"' + + def format_missing_error(self, name: str) -> str: + return Errors.ELEMENT_MISSING.format(self.render_label(name)) + + def format_mismatch_error(self, name: str, expected: str, actual: str) -> str: + return Errors.ELEMENT_MISMATCH.format(self.render_label(name), expected, actual) + + def format_invalid_error(self, name: str, allowed: Union[Sequence[str], str], found: str) -> str: + return Errors.ELEMENT_INVALID.format(self.render_label(name), allowed, found) + + +class TypedVariableContext(SimpleVariableContext): + """ + Context for typed variables defined in code. + + Used for variables with type annotations, validated both for declared value + and expected type compliance. + """ + + def __init__(self): + super().__init__(Constants.SCOPE_VARIABLE) + + def render_label(self, name: str) -> str: + """ + Create a human-readable label for the variable. + + Parameters + ---------- + name : str + The name of the variable. + + Returns + ------- + str + A formatted string like 'The variable "x"'. + """ + return f'The variable "{name}"' + + def format_invalid_error(self, allowed: Union[Sequence[str], str], found: str) -> str: + return Errors.ELEMENT_INVALID.format("The annotation", allowed, found) + + +class AttributeContext(TypedVariableContext): + """ + Context for object or class attributes. + + Used when validating attributes that belong to classes or their instances, + whether typed or not. + """ + + def __init__(self, classname: str): + self.classname = classname + super().__init__(Constants.SCOPE_CLASS_ATTRIBUTE) + + def render_label(self, type: str, name: str) -> str: + """ + Create a human-readable label for a class or instance attribute. + + Parameters + ---------- + type : str + The attribute type ("class" or "instance"). + name : str + The attribute name. + + Returns + ------- + str + A formatted string like 'The class attribute "foo" of class MyClass'. + """ + return f'The {type} attribute "{name}" of class {self.classname}' + + +class FunctionArgumentContext(AttributeContext): + """ + Context for arguments declared in functions. + + It includes the name of the function to provide scope-specific error messaging + for each declared parameter. + """ + + def __init__(self, function_name: str): + self.function_name = function_name + super().__init__(Constants.SCOPE_FUNCTION_ARG) + + def render_label(self, name: str) -> str: + """ + Create a label for a function argument. + + Parameters + ---------- + name : str + Argument name. + + Returns + ------- + str + A formatted string like 'The argument "x" of function "process"'. + """ + return f'The argument "{name}" of function "{self.function_name}"' + + +class MethodArgumentContext(FunctionArgumentContext): + """ + Context for arguments declared in class methods. + + Adds class name to scope representation to form fully qualified labels. + """ + + def __init__(self, classname: str): + self.classname = classname + super().__init__(Constants.SCOPE_METHOD_ARG_IN_CLASS) + + def render_label(self, name: str, method_name: str) -> str: + """ + Create a label for a method argument within a class. + + Parameters + ---------- + name : str + Argument name. + method_name : str + Method name. + + Returns + ------- + str + A formatted string like 'The argument "id" of method "save" of class "User"'. + """ + return ( + f'The argument "{name}" of method "{method_name}" ' + f'of class "{self.classname}"' + ) diff --git a/src/importspy/errors.py b/src/importspy/errors.py index a60ad3a..a0f823a 100644 --- a/src/importspy/errors.py +++ b/src/importspy/errors.py @@ -1,13 +1,26 @@ -from .constants import Scope +""" +importspy.errors +================ + +Defines standardized error message templates used across ImportSpy's +validation engine. These are grouped into three main categories: + +- ELEMENT_MISSING: when an expected element is declared but not found. +- ELEMENT_MISMATCH: when an element exists but does not match the contract. +- ELEMENT_INVALID: when an element has a value not permitted by the contract. + +These templates are used by Context classes and validation logic to produce +clear and consistent error messages during runtime or CLI contract validation. +""" class Errors: """ Central repository for error messages used in ImportSpy’s validation engine. - All validation errors extend one of three generic templates: - - ELEMENT_MISSING: expected but not found - - ELEMENT_MISMATCH: found but does not match the declared contract - - ELEMENT_INVALID: found but not allowed (invalid value from limited set) + Each validation error extends one of three base templates: + - ELEMENT_MISSING: used when an expected item is missing entirely. + - ELEMENT_MISMATCH: used when a found item differs from the declared value. + - ELEMENT_INVALID: used when the value is not allowed from a predefined set. """ # General Warnings @@ -16,7 +29,7 @@ class Errors: "to prevent stack overflow or performance issues." ) - # Generic Templates + # Generic Error Templates ELEMENT_MISSING = ( "{0} is declared but missing in the system. " "Ensure it is properly defined and implemented." @@ -34,7 +47,7 @@ class Errors: "Update the environment or contract accordingly." ) - # Module Validation Errors + # Specific Module-Level Validations (not scoped by Context) FILENAME_MISMATCH = ELEMENT_MISMATCH.format( "The module filename", "{0}", "{1}" ) @@ -42,23 +55,16 @@ class Errors: "The module version", "{0}", "{1}" ) - # Function and Class Validation + # Function and Class Validations FUNCTION_RETURN_ANNOTATION_MISMATCH = ELEMENT_MISMATCH.format( "The return annotation of {0} '{1}'", "{2}", "{3}" ) - CLASS_MISSING = ELEMENT_MISSING.format('The class "{0}"') CLASS_SUPERCLASS_MISSING = ELEMENT_MISSING.format( 'The superclass "{0}" in class "{1}"' ) - # Annotation and Runtime Validation - INVALID_ANNOTATION = ELEMENT_INVALID.format( - "The annotation", "{allowed}", "{found}" - ) - INVALID_ATTRIBUTE_TYPE = ELEMENT_INVALID.format( - "The attribute type", "{allowed}", "{found}" - ) + # Runtime / Environment Constraint Violations INVALID_ARCHITECTURE = ELEMENT_INVALID.format( "The system architecture", "{allowed}", "{found}" ) @@ -71,53 +77,3 @@ class Errors: INVALID_PYTHON_INTERPRETER = ELEMENT_INVALID.format( "The Python interpreter", "{allowed}", "{found}" ) - - # Scoped MISSING Errors - VARIABLE_MISSING_ERROR = ELEMENT_MISSING.format('The variable "{name}"') - ENV_VAR_MISSING_ERROR = ELEMENT_MISSING.format('The environment variable "{name}"') - FUNCTION_ARG_MISSING_ERROR = ELEMENT_MISSING.format('The argument "{name}" of function "{function}"') - METHOD_ARG_IN_CLASS_MISSING_ERROR = ELEMENT_MISSING.format( - 'The argument "{name}" of method "{method}" of class "{class_name}"' - ) - CLASS_ATTRIBUTE_MISSING_ERROR = ELEMENT_MISSING.format( - 'The class attribute "{name}" of class "{class_name}"' - ) - INSTANCE_ATTRIBUTE_MISSING_ERROR = ELEMENT_MISSING.format( - 'The instance attribute "{name}" of class "{class_name}"' - ) - - # Scoped MISMATCH Errors - VARIABLE_MISMATCH_ERROR = ELEMENT_MISMATCH.format('The variable "{name}"', "{expected}", "{actual}") - ENV_VAR_MISMATCH_ERROR = ELEMENT_MISMATCH.format('The environment variable "{name}"', "{expected}", "{actual}") - FUNCTION_ARG_MISMATCH_ERROR = ELEMENT_MISMATCH.format( - 'The argument "{name}" of function "{function}"', "{expected}", "{actual}" - ) - METHOD_ARG_IN_CLASS_MISMATCH_ERROR = ELEMENT_MISMATCH.format( - 'The argument "{name}" of method "{method}" of class "{class_name}"', - "{expected}", "{actual}" - ) - CLASS_ATTRIBUTE_MISMATCH_ERROR = ELEMENT_MISMATCH.format( - 'The class attribute "{name}" of class "{class_name}"', "{expected}", "{actual}" - ) - INSTANCE_ATTRIBUTE_MISMATCH_ERROR = ELEMENT_MISMATCH.format( - 'The instance attribute "{name}" of class "{class_name}"', "{expected}", "{actual}" - ) - - # Scope-to-message mapping - SCOPE_ELEMENT_MISSING_ERRORS = { - Scope.SCOPE_VARIABLE: VARIABLE_MISSING_ERROR, - Scope.SCOPE_ENVIRONMENT: ENV_VAR_MISSING_ERROR, - Scope.SCOPE_FUNCTION_ARG: FUNCTION_ARG_MISSING_ERROR, - Scope.SCOPE_METHOD_ARG_IN_CLASS: METHOD_ARG_IN_CLASS_MISSING_ERROR, - Scope.SCOPE_CLASS_ATTRIBUTE: CLASS_ATTRIBUTE_MISSING_ERROR, - Scope.SCOPE_INSTANCE_ATTRIBUTE: INSTANCE_ATTRIBUTE_MISSING_ERROR, - } - - SCOPE_ELEMENT_MISMATCH_ERRORS = { - Scope.SCOPE_VARIABLE: VARIABLE_MISMATCH_ERROR, - Scope.SCOPE_ENVIRONMENT: ENV_VAR_MISMATCH_ERROR, - Scope.SCOPE_FUNCTION_ARG: FUNCTION_ARG_MISMATCH_ERROR, - Scope.SCOPE_METHOD_ARG_IN_CLASS: METHOD_ARG_IN_CLASS_MISMATCH_ERROR, - Scope.SCOPE_CLASS_ATTRIBUTE: CLASS_ATTRIBUTE_MISMATCH_ERROR, - Scope.SCOPE_INSTANCE_ATTRIBUTE: INSTANCE_ATTRIBUTE_MISMATCH_ERROR, - } diff --git a/src/importspy/validators/argument_validator.py b/src/importspy/validators/argument_validator.py deleted file mode 100644 index a8eb027..0000000 --- a/src/importspy/validators/argument_validator.py +++ /dev/null @@ -1,162 +0,0 @@ -""" -importspy.validators.argument_validator -======================================= - -This module provides validation for function or method arguments -within Python modules being inspected by ImportSpy. - -The `ArgumentValidator` compares declared arguments from the import contract -against the actual arguments found in the target module, ensuring: -- Name consistency -- Type annotation compliance -- Default value consistency - -This validator is typically called from FunctionValidator or ClassValidator -as part of a full SpyModel validation. -""" - -from ..models import Argument -from ..errors import Errors -from ..constants import Constants -from typing import Optional, List -from importspy.log_manager import LogManager - - -class ArgumentValidator: - """ - Validates argument definitions within functions or methods. - - This class ensures that each expected argument matches its actual counterpart - in terms of name, type annotation, and default value. - - Attributes - ---------- - logger : logging.Logger - Internal logger used for debug output. - - Methods - ------- - validate(arguments_1, arguments_2, function_name, class_name="") - Compare two sets of arguments and raise errors for mismatches. - """ - - def __init__(self): - """ - Initialize the ArgumentValidator with a scoped logger. - """ - self.logger = LogManager().get_logger(self.__class__.__name__) - - def validate( - self, - arguments_1: List[Argument], - arguments_2: List[Argument], - function_name: str, - class_name: Optional[str] = "" - ): - """ - Validate function or method arguments for name, type, and value compliance. - - Parameters - ---------- - arguments_1 : List[Argument] - List of expected arguments defined in the import contract. - - arguments_2 : List[Argument] - List of actual arguments found in the inspected module. - - function_name : str - The name of the function or method being validated. - - class_name : Optional[str], default="" - The name of the class containing the method (if any), used for error context. - - Returns - ------- - bool - True if validation passes without raising an exception. - - Raises - ------ - ValueError - - If expected arguments are missing. - - If type annotations mismatch. - - If default values differ. - - Example - ------- - >>> validator = ArgumentValidator() - >>> validator.validate( - ... arguments_1=[Argument(name="x", annotation="int")], - ... arguments_2=[Argument(name="x", annotation="int")], - ... function_name="my_function" - ... ) - True - """ - context_name = f"method {function_name}" if class_name else f"function {function_name}" - - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Attribute validating", - status="Starting", - details=f"Expected attributes: {arguments_1} ; Current attributes: {arguments_2}" - ) - ) - - if not arguments_1: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Check if arguments_1 is not none", - status="Finished", - details=f"No declared arguments to validate; arguments_1: {arguments_1}" - ) - ) - return - - if not arguments_2: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Checking arguments_2 when arguments_1 is missing", - status="Finished", - details=f"No actual arguments found; arguments_2: {arguments_2}" - ) - ) - raise ValueError(Errors.ELEMENT_MISSING.format(arguments_1)) - - for argument_1 in arguments_1: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Argument validating", - status="Progress", - details=f"Current argument_1: {argument_1}" - ) - ) - if argument_1.name not in set(arg.name for arg in arguments_2): - raise ValueError(Errors.ARGUMENT_MISSING.format(argument_1.name, context_name)) - - for argument_1 in arguments_1: - argument_2 = next((arg for arg in arguments_2 if arg.name == argument_1.name), None) - - if not argument_2: - raise ValueError(Errors.ELEMENT_MISSING.format(argument_1)) - - if argument_1.annotation and argument_1.annotation != argument_2.annotation: - raise ValueError( - Errors.ARGUMENT_MISMATCH.format( - Constants.ANNOTATION, - argument_1.name, - argument_1.annotation, - argument_2.annotation - ) - ) - - if argument_1.value != argument_2.value: - raise ValueError( - Errors.ARGUMENT_MISMATCH.format( - Constants.VALUE, - argument_1.name, - argument_1.value, - argument_2.value - ) - ) - - return True diff --git a/src/importspy/validators/attribute_validator.py b/src/importspy/validators/attribute_validator.py deleted file mode 100644 index 9dcc3b8..0000000 --- a/src/importspy/validators/attribute_validator.py +++ /dev/null @@ -1,172 +0,0 @@ -""" -importspy.validators.attribute_validator -======================================== - -This module implements the validation logic for class and instance attributes -within modules inspected by ImportSpy. - -The `AttributeValidator` ensures that attributes declared in the import contract -match the ones actually defined in the module under inspection in terms of: -- Existence -- Type annotation -- Default value -""" - -from ..models import Attribute -from ..errors import Errors -from ..constants import Constants -from typing import List -from importspy.log_manager import LogManager - - -class AttributeValidator: - """ - Validator for class and instance attributes. - - Compares expected attributes (from the import contract) with those - extracted from the inspected module. Ensures attribute names, - annotations, and values are consistent. - - Attributes - ---------- - logger : logging.Logger - Internal logger used for debug tracing during validation. - - Methods - ------- - validate(attrs_1, attrs_2, classname) - Performs full validation of attributes for a given class. - """ - - def __init__(self): - """ - Initializes the AttributeValidator with scoped logging. - """ - self.logger = LogManager().get_logger(self.__class__.__name__) - - def validate( - self, - attrs_1: List[Attribute], - attrs_2: List[Attribute], - classname: str - ): - """ - Validates expected vs actual attributes in a class definition. - - Parameters - ---------- - attrs_1 : List[Attribute] - List of attributes defined in the import contract. - - attrs_2 : List[Attribute] - List of attributes found in the actual module. - - classname : str - The name of the class whose attributes are being validated. - - Returns - ------- - bool - True if all attributes match expectations. - - Raises - ------ - ValueError - - If required attributes are missing. - - If type annotations differ. - - If attribute values differ. - - Example - ------- - >>> validator = AttributeValidator() - >>> validator.validate( - ... attrs_1=[Attribute(name="path", value="/tmp", annotation="str", type="class")], - ... attrs_2=[Attribute(name="path", value="/tmp", annotation="str", type="class")], - ... classname="Config" - ... ) - True - """ - self.logger.debug(f"Type of attrs_1: {type(attrs_1)}") - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Attribute validating", - status="Starting", - details=f"Expected attributes: {attrs_1} ; Current attributes: {attrs_2}" - ) - ) - - if not attrs_1: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Check if attrs_1 is not none", - status="Finished", - details=f"No expected attributes; attrs_1: {attrs_1}" - ) - ) - return - - if not attrs_2: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Checking attrs_2 when attrs_1 is missing", - status="Finished", - details=f"No actual attributes found; attrs_2: {attrs_2}" - ) - ) - raise ValueError(Errors.ELEMENT_MISSING.format(attrs_1)) - - for attr_1 in attrs_1: - self.logger.debug(f"Type of attr_1: {type(attr_1)}") - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Attribute validating", - status="Progress", - details=f"Current attr_1: {attr_1}" - ) - ) - if attr_1.name not in {attr.name for attr in attrs_2}: - self.logger.debug(Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Checking if attr_1 is in attrs_2", - status="Finished", - details=Errors.CLASS_ATTRIBUTE_MISSING.format( - attr_1.type, - f"{attr_1.name}={attr_1.value}", - classname - ) - )) - raise ValueError( - Errors.CLASS_ATTRIBUTE_MISSING.format( - attr_1.type, - f"{attr_1.name}={attr_1.value}", - classname - ) - ) - - for attr_1 in attrs_1: - attr_2 = next((attr for attr in attrs_2 if attr.name == attr_1.name), None) - if not attr_2: - raise ValueError(Errors.ELEMENT_MISSING.format(attrs_1)) - - if attr_1.annotation and attr_1.annotation != attr_2.annotation: - raise ValueError( - Errors.CLASS_ATTRIBUTE_MISMATCH.format( - Constants.ANNOTATION, - attr_1.type, - attr_1.name, - attr_1.annotation, - attr_2.annotation - ) - ) - - if attr_1.value != attr_2.value: - raise ValueError( - Errors.CLASS_ATTRIBUTE_MISMATCH.format( - Constants.VALUE, - attr_1.type, - attr_1.name, - attr_1.value, - attr_2.value - ) - ) - - return True diff --git a/src/importspy/validators/function_validator.py b/src/importspy/validators/function_validator.py index 6cb63b4..509d41d 100644 --- a/src/importspy/validators/function_validator.py +++ b/src/importspy/validators/function_validator.py @@ -1,83 +1,30 @@ -""" -importspy.validators.function_validator -======================================= - -Validator for function declarations and signatures. - -This module defines the `FunctionValidator`, responsible for verifying that functions -defined in a Python module (or class) match those specified in an import contract. -It ensures that: -- Each expected function is present. -- Return annotations are correct. -- Function arguments match in name, annotation, and value. - -This validator uses `ArgumentValidator` to validate function arguments. -""" - from ..models import Function from ..errors import Errors from ..constants import Constants from typing import List, Optional from ..log_manager import LogManager -from .argument_validator import ArgumentValidator +from .variable_validator import VariableValidator +from ..contexts import ( + Context, + MethodArgumentContext +) class FunctionValidator: - """ - Validator for function declarations and signatures. - Attributes - ---------- - logger : logging.Logger - Logger used for debug tracing. - _argument_validator : ArgumentValidator - Helper validator to handle argument validation. - """ + def __init__(self, context: Optional[Context]): - def __init__(self): - """ - Initialize the function validator and argument checker. - """ self.logger = LogManager().get_logger(self.__class__.__name__) - self._argument_validator = ArgumentValidator() + current_context = Context(Constants.SCOPE_FUNCTION_ARG) + if context: + current_context = context(MethodArgumentContext) + self._variable_validator = VariableValidator(context=current_context) def validate( self, functions_1: List[Function], functions_2: List[Function], - classname: Optional[str] = "" ) -> Optional[bool]: - """ - Validate a list of expected functions against actual module functions. - - Parameters - ---------- - functions_1 : List[Function] - The list of expected functions (from import contract). - functions_2 : List[Function] - The actual functions extracted from the module. - classname : Optional[str], default="" - If validating class methods, the class name (for error context). - - Returns - ------- - Optional[bool] - True if validation passes, None if nothing to validate. - - Raises - ------ - ValueError - If: - - A function is missing. - - Return annotations differ. - - Argument validation fails. - - Example - ------- - >>> validator = FunctionValidator() - >>> validator.validate(expected_functions, actual_functions, classname="MyService") - """ - context_name = f"method in class {classname}" if classname else "function" self.logger.debug( Constants.LOG_MESSAGE_TEMPLATE.format( @@ -124,7 +71,7 @@ def validate( ) ) raise ValueError( - Errors.FUNCTIONS_MISSING.format(context_name, function_1.name) + Errors.ELEMENT_MISMATCH.format(context_name, function_1.name) ) for function_1 in functions_1: @@ -142,7 +89,6 @@ def validate( if function_1.return_annotation and function_1.return_annotation != function_2.return_annotation: raise ValueError( Errors.FUNCTION_RETURN_ANNOTATION_MISMATCH.format( - context_name, function_1.name, function_1.return_annotation, function_2.return_annotation diff --git a/src/importspy/validators/system_validator.py b/src/importspy/validators/system_validator.py index 139fe0b..7bf6c81 100644 --- a/src/importspy/validators/system_validator.py +++ b/src/importspy/validators/system_validator.py @@ -20,8 +20,9 @@ ) from ..errors import Errors from .python_validator import PythonValidator +from ..constants import Constants from .variable_validator import VariableValidator - +from ..contexts import SimpleVariableContext class SystemValidator: """ @@ -99,7 +100,7 @@ def validate( class EnvironmentValidator: def __init__(self): - self._variable_validator = VariableValidator() + self._variable_validator = VariableValidator(context=SimpleVariableContext(Constants.SCOPE_ENVIRONMENT)) def validate(self, environment_1:Environment, diff --git a/src/importspy/validators/variable_validator.py b/src/importspy/validators/variable_validator.py index 1d3281d..37fa2b6 100644 --- a/src/importspy/validators/variable_validator.py +++ b/src/importspy/validators/variable_validator.py @@ -1,13 +1,51 @@ -from ..log_manager import LogManager -from ..models import Variable -from ..constants import Constants -from ..errors import Errors +""" +importspy.validators.variable_validator +======================================= + +This module provides validation logic for variables, including standalone variables, +function arguments, class attributes, and environment variables. + +Each validation runs in the context of a specific `Context` subclass, which encapsulates +label rendering and scoped error formatting for missing, mismatched, or invalid elements. + +Author: ImportSpy Team +License: MIT +""" + from typing import List +from importspy.log_manager import LogManager +from importspy.models import Variable +from importspy.constants import Constants +from importspy.contexts import Context + class VariableValidator: + """ + Validates lists of `Variable` instances within a given scope-aware context. + + This class compares expected and actual variables for: + - Presence (missing variables) + - Value equality + - Annotation correctness (for typed contexts) + + Attributes + ---------- + context : Context + A `Context` subclass instance that defines how to format errors and describe the scope. + logger : logging.Logger + Logger used for structured debug output. + """ - def __init__(self, scope: str): - self.scope = scope + def __init__(self, context: Context): + """ + Initialize the validator with a context. + + Parameters + ---------- + context : Context + A context object that encapsulates the validation scope and error message logic. + """ + self.context = context self.logger = LogManager().get_logger(self.__class__.__name__) def validate( @@ -15,13 +53,28 @@ def validate( variables_1: List[Variable], variables_2: List[Variable], ): - self.logger.debug(f"Scope of valitadion: {self.scope}") + """ + Validate two sets of variables for presence, value match, and type annotations. + + Parameters + ---------- + variables_1 : List[Variable] + The list of expected variables (from the contract). + variables_2 : List[Variable] + The list of actual variables (from the module/system). + + Raises + ------ + ValueError + If a variable is missing, has a mismatched value, or fails type annotation validation. + """ + self.logger.debug(f"Context: {self.context.__class__.__name__} (scope={self.context.scope})") self.logger.debug(f"Type of variables_1: {type(variables_1)}") self.logger.debug( Constants.LOG_MESSAGE_TEMPLATE.format( operation="Variable validating", status="Starting", - details=f"Expected Variables: {variables_1} in {self.scope} scope ; Current Variables: {variables_2} in {self.scope} scope" + details=f"Expected Variables: {variables_1} ; Actual Variables: {variables_2}" ) ) @@ -30,7 +83,7 @@ def validate( Constants.LOG_MESSAGE_TEMPLATE.format( operation="Check if variables_1 is not none", status="Finished", - details=f"No expected Variables in {self.scope} scope; variables_1: {variables_1} in {self.scope} scope" + details="No expected Variables to validate" ) ) return @@ -40,10 +93,10 @@ def validate( Constants.LOG_MESSAGE_TEMPLATE.format( operation="Checking variables_2 when variables_1 is missing", status="Finished", - details=f"No actual Variables in found in {self.scope} scope; variables_2: {variables_2} in {self.scope} scope" + details="No actual Variables found for validation" ) ) - raise ValueError(Errors.ELEMENT_MISSING.format(f"{variables_1}")) + raise ValueError(self.context.format_missing_error()) for vars_1 in variables_1: self.logger.debug( @@ -54,40 +107,21 @@ def validate( ) ) if vars_1.name not in {var.name for var in variables_2}: - self.logger.debug(Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Checking if vars_1 is in variables_2", - status="Finished", - details=Errors.VARIABLE_MISSING.format( - f"{vars_1.name}={vars_1.value}" - ) - )) - raise ValueError( - Errors.VARIABLE_MISSING.format( - f"{vars_1.name}={vars_1.value}" - ) - ) + raise ValueError(self.context.format_missing_error()) for vars_1 in variables_1: vars_2 = next((var for var in variables_2 if var.name == vars_1.name), None) if not vars_2: - raise ValueError(Errors.ELEMENT_MISSING.format(variables_1)) + raise ValueError(self.context.format_missing_error()) - if vars_1.annotation and vars_1.annotation != vars_2.annotation: + # Annotation mismatch (only if not validating env vars) + if self.context.scope != Constants.SCOPE_ENVIRONMENT and vars_1.annotation and vars_1.annotation != vars_2.annotation: raise ValueError( - Errors.VARIABLE_MISMATCH.format( - Constants.ANNOTATION, - vars_1.name, - vars_1.annotation, - vars_2.annotation - ) + self.context.format_mismatch_error(vars_1.annotation, vars_2.annotation) ) + # Value mismatch if vars_1.value != vars_2.value: raise ValueError( - Errors.VARIABLE_MISMATCH.format( - Constants.VALUE, - vars_1.name, - vars_1.value, - vars_2.value - ) - ) \ No newline at end of file + self.context.format_mismatch_error(vars_1.value, vars_2.value) + ) From 01e7deaac0860adc91a4e5d82c90ba4264ad0912 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Wed, 14 May 2025 15:59:10 +0200 Subject: [PATCH 04/40] refactor(error-handling): unify validation errors into centralized, context-aware template engine - Replaced legacy error formatting with structured, reusable error templates - Moved error definitions and context category mappings into constants.py - Removed deprecated validators (argument_validator, attribute_validator, common_validator) - Deprecated static error classes in favor of dynamic Error model generation - Introduced Contexts enum and error intros for modular, class, runtime, and environment scopes - Simplified and cleaned validation logic across modules and models - Removed legacy context classes in preparation for new context bundling layer Next step: Introduce ContextBundle classes to persist contextual state during validation, enabling precise scope-aware diagnostics (e.g. method argument inside class, or env var at runtime). --- src/importspy/constants.py | 66 +++++- src/importspy/contexts.py | 202 ------------------ src/importspy/errors.py | 79 ------- src/importspy/models.py | 93 ++++---- src/importspy/validators/common_validator.py | 99 --------- .../validators/function_validator.py | 8 +- src/importspy/validators/module_validator.py | 2 - .../validators/spymodel_validator.py | 4 +- src/importspy/validators/system_validator.py | 6 +- .../validators/variable_validator.py | 17 +- 10 files changed, 123 insertions(+), 453 deletions(-) delete mode 100644 src/importspy/contexts.py delete mode 100644 src/importspy/errors.py delete mode 100644 src/importspy/validators/common_validator.py diff --git a/src/importspy/constants.py b/src/importspy/constants.py index 98bdd86..8058966 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -1,4 +1,5 @@ from .config import Config +from enum import Enum class Constants: @@ -130,8 +131,65 @@ class Constants: "[Details: {details}]" ) +class Contexts(str, Enum): + + RUNTIME_CONTEXT = "runtime" + ENVIRONMENT_CONTEXT = "environment" + MODULE_CONTEXT = "module" + CLASS_CONTEXT = "class" + +class Errors: + """ + Defines reusable templates for error generation. + """ + + TEMPLATE_KEY = "template" + SOLUTION_KEY = "solution" SCOPE_VARIABLE = "variable" - SCOPE_ENVIRONMENT = "environment" - SCOPE_FUNCTION_ARG = "function_arg" - SCOPE_METHOD_ARG_IN_CLASS = "method_arg_in_class" - SCOPE_CLASS_ATTRIBUTE = "class_attribute" + SCOPE_ARGUMENT = "argument" + + CONTEXT_INTRO = { + Contexts.RUNTIME_CONTEXT: "Runtime constraint violation", + Contexts.ENVIRONMENT_CONTEXT: "Environment validation failure", + Contexts.MODULE_CONTEXT: "Module structural inconsistency", + Contexts.CLASS_CONTEXT: "Class contract violation" + } + + class Category(str, Enum): + + MISSING = "missing" + MISMATCH = "mismatch" + INVALID = "invalid" + + VARIABLES_TEMPLATE = { + + SCOPE_VARIABLE: { + Contexts.ENVIRONMENT_CONTEXT: 'The environment variable "{name}"', + Contexts.MODULE_CONTEXT: 'The variable "{name}" in module "{module}"', + Contexts.CLASS_CONTEXT: 'The {attribute_type} attribute "{name}" in class "{class_name}"' + }, + SCOPE_ARGUMENT: { + Contexts.MODULE_CONTEXT: 'The argument "{name}" of function "{function_name}"', + Contexts.CLASS_CONTEXT: 'The argument "{name}" of method "{method_name}" in class "{class_name}"', + } + } + + FUNCTIONS_TEMPLATE = { + Contexts.MODULE_CONTEXT: 'The function "{name}" in module "{module}"', + Contexts.CLASS_CONTEXT: 'The method "{name}" in class "{class_name}"' + } + + ERROR_MESSAGE_TEMPLATES = { + Category.MISSING: { + TEMPLATE_KEY: "{label} is declared but missing.", + SOLUTION_KEY: "Ensure it is properly defined and implemented." + }, + Category.MISMATCH: { + TEMPLATE_KEY: "{label} does not match the expected value. Expected: {expected!r}, Found: {actual!r}.", + SOLUTION_KEY: "Check the implementation or update the contract accordingly." + }, + Category.INVALID: { + TEMPLATE_KEY: "{label} has an invalid value. Allowed values: {allowed}. Found: {found!r}.", + SOLUTION_KEY: "Update the environment or contract accordingly." + } + } \ No newline at end of file diff --git a/src/importspy/contexts.py b/src/importspy/contexts.py deleted file mode 100644 index a3ae0da..0000000 --- a/src/importspy/contexts.py +++ /dev/null @@ -1,202 +0,0 @@ -""" -importspy.context -================= - -This module defines context-aware validation classes used in ImportSpy -to generate precise and meaningful error messages during contract enforcement. - -Each context class encapsulates the structural role of an element -(such as a variable, function argument, or class attribute) and provides -methods to render human-readable labels and formatted error strings. - -By localizing the rendering logic and scope-specific semantics, -these classes ensure clarity, traceability, and consistency in validation reports. - -These context-aware abstractions are essential to separate structural expectations -from dynamic enforcement, giving ImportSpy the ability to issue expressive, -actionable diagnostics across scopes and validation levels. -""" - -from typing import Union, Sequence -from .errors import Errors -from .constants import Constants - - -class Context: - """ - Abstract base class for all validation contexts in ImportSpy. - - A context defines the semantic and structural scope of an element under validation. - It provides interface methods for generating context-aware error messages - related to missing, mismatched, or invalid elements. - """ - - def __init__(self, scope: str): - """ - Parameters - ---------- - scope : str - A Scope constant string that identifies the validation context type. - """ - self.scope = scope - - -class SimpleVariableContext(Context): - """ - Context for variables without type annotations. - - Typically used for simple key-value definitions such as environment variables, - module-level constants, or untyped configuration fields. - """ - - def __init__(self): - super().__init__(Constants.SCOPE_ENVIRONMENT) - - def render_label(self, name: str) -> str: - """ - Create a human-readable label for the variable. - - Parameters - ---------- - name : str - The name of the variable. - - Returns - ------- - str - A formatted string like 'The environment variable "FOO"'. - """ - return f'The environment variable "{name}"' - - def format_missing_error(self, name: str) -> str: - return Errors.ELEMENT_MISSING.format(self.render_label(name)) - - def format_mismatch_error(self, name: str, expected: str, actual: str) -> str: - return Errors.ELEMENT_MISMATCH.format(self.render_label(name), expected, actual) - - def format_invalid_error(self, name: str, allowed: Union[Sequence[str], str], found: str) -> str: - return Errors.ELEMENT_INVALID.format(self.render_label(name), allowed, found) - - -class TypedVariableContext(SimpleVariableContext): - """ - Context for typed variables defined in code. - - Used for variables with type annotations, validated both for declared value - and expected type compliance. - """ - - def __init__(self): - super().__init__(Constants.SCOPE_VARIABLE) - - def render_label(self, name: str) -> str: - """ - Create a human-readable label for the variable. - - Parameters - ---------- - name : str - The name of the variable. - - Returns - ------- - str - A formatted string like 'The variable "x"'. - """ - return f'The variable "{name}"' - - def format_invalid_error(self, allowed: Union[Sequence[str], str], found: str) -> str: - return Errors.ELEMENT_INVALID.format("The annotation", allowed, found) - - -class AttributeContext(TypedVariableContext): - """ - Context for object or class attributes. - - Used when validating attributes that belong to classes or their instances, - whether typed or not. - """ - - def __init__(self, classname: str): - self.classname = classname - super().__init__(Constants.SCOPE_CLASS_ATTRIBUTE) - - def render_label(self, type: str, name: str) -> str: - """ - Create a human-readable label for a class or instance attribute. - - Parameters - ---------- - type : str - The attribute type ("class" or "instance"). - name : str - The attribute name. - - Returns - ------- - str - A formatted string like 'The class attribute "foo" of class MyClass'. - """ - return f'The {type} attribute "{name}" of class {self.classname}' - - -class FunctionArgumentContext(AttributeContext): - """ - Context for arguments declared in functions. - - It includes the name of the function to provide scope-specific error messaging - for each declared parameter. - """ - - def __init__(self, function_name: str): - self.function_name = function_name - super().__init__(Constants.SCOPE_FUNCTION_ARG) - - def render_label(self, name: str) -> str: - """ - Create a label for a function argument. - - Parameters - ---------- - name : str - Argument name. - - Returns - ------- - str - A formatted string like 'The argument "x" of function "process"'. - """ - return f'The argument "{name}" of function "{self.function_name}"' - - -class MethodArgumentContext(FunctionArgumentContext): - """ - Context for arguments declared in class methods. - - Adds class name to scope representation to form fully qualified labels. - """ - - def __init__(self, classname: str): - self.classname = classname - super().__init__(Constants.SCOPE_METHOD_ARG_IN_CLASS) - - def render_label(self, name: str, method_name: str) -> str: - """ - Create a label for a method argument within a class. - - Parameters - ---------- - name : str - Argument name. - method_name : str - Method name. - - Returns - ------- - str - A formatted string like 'The argument "id" of method "save" of class "User"'. - """ - return ( - f'The argument "{name}" of method "{method_name}" ' - f'of class "{self.classname}"' - ) diff --git a/src/importspy/errors.py b/src/importspy/errors.py deleted file mode 100644 index a0f823a..0000000 --- a/src/importspy/errors.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -importspy.errors -================ - -Defines standardized error message templates used across ImportSpy's -validation engine. These are grouped into three main categories: - -- ELEMENT_MISSING: when an expected element is declared but not found. -- ELEMENT_MISMATCH: when an element exists but does not match the contract. -- ELEMENT_INVALID: when an element has a value not permitted by the contract. - -These templates are used by Context classes and validation logic to produce -clear and consistent error messages during runtime or CLI contract validation. -""" - -class Errors: - """ - Central repository for error messages used in ImportSpy’s validation engine. - - Each validation error extends one of three base templates: - - ELEMENT_MISSING: used when an expected item is missing entirely. - - ELEMENT_MISMATCH: used when a found item differs from the declared value. - - ELEMENT_INVALID: used when the value is not allowed from a predefined set. - """ - - # General Warnings - ANALYSIS_RECURSION_WARNING = ( - "Warning: Analysis recursion detected. Avoid analyzing code that itself handles analysis, " - "to prevent stack overflow or performance issues." - ) - - # Generic Error Templates - ELEMENT_MISSING = ( - "{0} is declared but missing in the system. " - "Ensure it is properly defined and implemented." - ) - - ELEMENT_MISMATCH = ( - "{0} is defined but its value does not match the expected one. " - "Expected: {1!r}, Found: {2!r}. " - "Check the implementation and update the contract or the code accordingly." - ) - - ELEMENT_INVALID = ( - "{0} has an invalid value. " - "Allowed values: {1}. Found: {2!r}. " - "Update the environment or contract accordingly." - ) - - # Specific Module-Level Validations (not scoped by Context) - FILENAME_MISMATCH = ELEMENT_MISMATCH.format( - "The module filename", "{0}", "{1}" - ) - VERSION_MISMATCH = ELEMENT_MISMATCH.format( - "The module version", "{0}", "{1}" - ) - - # Function and Class Validations - FUNCTION_RETURN_ANNOTATION_MISMATCH = ELEMENT_MISMATCH.format( - "The return annotation of {0} '{1}'", "{2}", "{3}" - ) - CLASS_MISSING = ELEMENT_MISSING.format('The class "{0}"') - CLASS_SUPERCLASS_MISSING = ELEMENT_MISSING.format( - 'The superclass "{0}" in class "{1}"' - ) - - # Runtime / Environment Constraint Violations - INVALID_ARCHITECTURE = ELEMENT_INVALID.format( - "The system architecture", "{allowed}", "{found}" - ) - INVALID_OS = ELEMENT_INVALID.format( - "The operating system", "{allowed}", "{found}" - ) - INVALID_PYTHON_VERSION = ELEMENT_INVALID.format( - "The Python version", "{allowed}", "{found}" - ) - INVALID_PYTHON_INTERPRETER = ELEMENT_INVALID.format( - "The Python interpreter", "{allowed}", "{found}" - ) diff --git a/src/importspy/models.py b/src/importspy/models.py index d1ca56e..68c4a1c 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -8,8 +8,16 @@ system-level metadata required to enforce import contracts across execution contexts. """ -from pydantic import BaseModel, field_validator, Field -from typing import Optional, List, Union +from pydantic import ( + BaseModel, + field_validator +) + +from typing import ( + Optional, + Union +) + from types import ModuleType from .utilities.module_util import ( @@ -19,8 +27,11 @@ from .utilities.runtime_util import RuntimeUtil from .utilities.system_util import SystemUtil from .utilities.python_util import PythonUtil -from .constants import Constants -from .errors import Errors +from .constants import ( + Constants, + Contexts, + Errors +) import logging logger = logging.getLogger("/".join(__file__.split('/')[-2:])) @@ -36,7 +47,7 @@ class Python(BaseModel): """ version: Optional[str] = None interpreter: Optional[str] = None - modules: List['Module'] + modules: list['Module'] @field_validator('version') def validate_version(cls, value: str): @@ -61,8 +72,8 @@ class Environment(BaseModel): Represents a set of environment variables and secret keys defined for the system or application runtime. """ - variables: Optional[List['Variable']] = None - secrets: Optional[List[str]] = None + variables: Optional[list['Variable']] = None + secrets: Optional[list[str]] = None class System(BaseModel): """ @@ -71,7 +82,7 @@ class System(BaseModel): """ os: str environment: Optional[Environment] = None - pythons: List[Python] + pythons: list[Python] @field_validator('os') def validate_os(cls, value: str): @@ -89,7 +100,7 @@ class Runtime(BaseModel): the list of supported systems associated with that architecture. """ arch: str - systems: List[System] + systems: list[System] @field_validator('arch') def validate_arch(cls, value: str): @@ -125,7 +136,7 @@ def validate_annotation(cls, value): return value @classmethod - def from_variable_info(cls, variables_info: List[VariableInfo]): + def from_variable_info(cls, variables_info: list[VariableInfo]): """ Convert a list of extracted VariableInfo into Variable instances. """ @@ -153,7 +164,7 @@ def validate_type(cls, value: str): return value @classmethod - def from_attributes_info(cls, attributes_info: List[AttributeInfo]): + def from_attributes_info(cls, attributes_info: list[AttributeInfo]): """ Convert a list of AttributeInfo objects into Attribute instances. """ @@ -171,7 +182,7 @@ class Argument(Variable, BaseModel): Used to validate callable structures and type consistency. """ @classmethod - def from_arguments_info(cls, arguments_info: List[ArgumentInfo]): + def from_arguments_info(cls, arguments_info: list[ArgumentInfo]): """ Convert a list of ArgumentInfo into Argument instances. """ @@ -188,7 +199,7 @@ class Function(BaseModel): and return type annotation. """ name: str - arguments: Optional[List[Argument]] = None + arguments: Optional[list[Argument]] = None return_annotation: Optional[str] = None @field_validator("return_annotation") @@ -199,7 +210,7 @@ def validate_annotation(cls, value): return CommonValidator.validate_annotation(value) @classmethod - def from_functions_info(cls, functions_info: List[FunctionInfo]): + def from_functions_info(cls, functions_info: list[FunctionInfo]): """ Convert a list of FunctionInfo into Function instances. """ @@ -216,12 +227,12 @@ class Class(BaseModel): Used to enforce object-level validation rules in contracts. """ name: str - attributes: Optional[List[Attribute]] = None - methods: Optional[List[Function]] = None - superclasses: Optional[List[str]] = None + attributes: Optional[list[Attribute]] = None + methods: Optional[list[Function]] = None + superclasses: Optional[list[str]] = None @classmethod - def from_class_info(cls, extracted_classes: List[ClassInfo]): + def from_class_info(cls, extracted_classes: list[ClassInfo]): """ Convert a list of extracted class definitions into Class instances. """ @@ -240,9 +251,9 @@ class Module(BaseModel): """ filename: Optional[str] = None version: Optional[str] = None - variables: Optional[List[Variable]] = None - functions: Optional[List[Function]] = None - classes: Optional[List[Class]] = None + variables: Optional[list[Variable]] = None + functions: Optional[list[Function]] = None + classes: Optional[list[Class]] = None class SpyModel(Module): @@ -252,7 +263,7 @@ class SpyModel(Module): SpyModel is the top-level object representing a module's structure and its runtime/environment constraints. This is the core of ImportSpy's contract model. """ - deployments: Optional[List[Runtime]] = None + deployments: Optional[list[Runtime]] = None @classmethod def from_module(cls, info_module: ModuleType): @@ -315,22 +326,28 @@ def from_module(cls, info_module: ModuleType): ] ) +class Error: -class CommonValidator: - """ - Provides shared validation utilities for type annotations and other structural elements. - """ + context: Contexts + title: str = Errors.CONTEXT_INTRO.get(context) + category: Errors.Category + description: str + solution: str @classmethod - def validate_annotation(cls, value): - """ - Validate a type annotation against the supported base annotations. - """ - if not value: - return None - base = value.split("[")[0] - if base not in Constants.SUPPORTED_ANNOTATIONS: - raise ValueError( - Errors.INVALID_ANNOTATION.format(value, Constants.SUPPORTED_ANNOTATIONS) - ) - return value + def from_template(cls, *, context: str, category: str, label: str, **kwargs): + tpl = Errors.ERROR_MESSAGE_TEMPLATES.get(category) + description = tpl[Errors.TEMPLATE_KEY].format(label=label, **kwargs) + solution = tpl[Errors.SOLUTION_KEY] + return cls( + context=context, + category=category, + description=description, + solution=solution + ) + + def render_message(self) -> str: + return f"[{self.title}] {self.description} {self.solution}" + + + diff --git a/src/importspy/validators/common_validator.py b/src/importspy/validators/common_validator.py deleted file mode 100644 index 4b569ee..0000000 --- a/src/importspy/validators/common_validator.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -importspy.validators.common_validator -===================================== - -Reusable validation logic for dictionary and list structures. - -This module defines the `CommonValidator` class, which provides utility methods to validate -data structures commonly used in contract inspection: -- General list containment validation -- Key/value consistency between dictionaries - -It is used across structural validators like: -- SystemValidator -- ModuleValidator -- RuntimeValidator -""" -from typing import List, Dict -from ..errors import Errors -from ..constants import Constants -from ..log_manager import LogManager - - -class CommonValidator: - """ - Common validation utilities for iterable structures. - - This helper class enables reusable checks across all ImportSpy validators. - It ensures list and dict structures match between expected (from `.yml`) - and actual (live modules) data. - - Validation Modes: - ----------------- - - list_validate(...) : All elements in list1 must exist in list2. - - dict_validate(...) : All key/value pairs in dict1 must match dict2. - - Attributes - ---------- - logger : logging.Logger - A scoped logger for structured output and debugging. - """ - - def __init__(self): - """ - Initializes the CommonValidator and its logger. - """ - self.logger = LogManager().get_logger(self.__class__.__name__) - - def list_validate( - self, - list1: List, - list2: List, - missing_error: str, - *args - ) -> None: - """ - Validates that all elements in `list1` exist in `list2`. - - Parameters - ---------- - list1 : List - The expected list of items. - list2 : List - The actual list to be validated. - missing_error : str - Error message format if an element is missing (e.g., "Missing: {0}"). - *args : tuple - Optional dynamic context passed to `missing_error.format(...)`. - - Raises - ------ - ValueError - If any element from `list1` is not present in `list2`. - - Returns - ------- - None - - Example - ------- - >>> CommonValidator().list_validate(["A", "B"], ["A"], "Missing item: {0}") - """ - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="List validating", - status="Starting", - details=f"Expected list: {list1} ; Current list: {list2}" - ) - ) - - if not list1: - return - if list1 and not list2: - return - if not list2: - raise ValueError(Errors.ELEMENT_MISSING.format(list1)) - - for expected_element in list1: - if expected_element not in list2: - raise ValueError(missing_error.format(expected_element, *args)) \ No newline at end of file diff --git a/src/importspy/validators/function_validator.py b/src/importspy/validators/function_validator.py index 509d41d..a5c0fe5 100644 --- a/src/importspy/validators/function_validator.py +++ b/src/importspy/validators/function_validator.py @@ -12,12 +12,8 @@ class FunctionValidator: - def __init__(self, context: Optional[Context]): + def __init__(self): - self.logger = LogManager().get_logger(self.__class__.__name__) - current_context = Context(Constants.SCOPE_FUNCTION_ARG) - if context: - current_context = context(MethodArgumentContext) self._variable_validator = VariableValidator(context=current_context) def validate( @@ -25,7 +21,7 @@ def validate( functions_1: List[Function], functions_2: List[Function], ) -> Optional[bool]: - + self.logger.debug( Constants.LOG_MESSAGE_TEMPLATE.format( operation="Function validating", diff --git a/src/importspy/validators/module_validator.py b/src/importspy/validators/module_validator.py index d8b6b3f..64cd96d 100644 --- a/src/importspy/validators/module_validator.py +++ b/src/importspy/validators/module_validator.py @@ -22,9 +22,7 @@ from ..models import Module from ..errors import Errors from .variable_validator import VariableValidator -from .attribute_validator import AttributeValidator from .function_validator import FunctionValidator -from .common_validator import CommonValidator from typing import List, Optional diff --git a/src/importspy/validators/spymodel_validator.py b/src/importspy/validators/spymodel_validator.py index a7d7327..fb57be8 100644 --- a/src/importspy/validators/spymodel_validator.py +++ b/src/importspy/validators/spymodel_validator.py @@ -81,10 +81,8 @@ def validate( >>> validator = SpyModelValidator() >>> validator.validate(spy_model_contract, spy_model_live) """ - # Validate runtime deployments self._runtime_validator.validate(spy_model_1.deployments, spy_model_2.deployments) - - # Navigate through the resolved runtime > system > python > module + self._module_validator.validate( [spy_model_1], spy_model_2 diff --git a/src/importspy/validators/system_validator.py b/src/importspy/validators/system_validator.py index 7bf6c81..e94b168 100644 --- a/src/importspy/validators/system_validator.py +++ b/src/importspy/validators/system_validator.py @@ -20,9 +20,7 @@ ) from ..errors import Errors from .python_validator import PythonValidator -from ..constants import Constants from .variable_validator import VariableValidator -from ..contexts import SimpleVariableContext class SystemValidator: """ @@ -100,10 +98,10 @@ def validate( class EnvironmentValidator: def __init__(self): - self._variable_validator = VariableValidator(context=SimpleVariableContext(Constants.SCOPE_ENVIRONMENT)) + self._variable_validator = VariableValidator(context="The environment variable") def validate(self, - environment_1:Environment, + environment_1: Environment, environment_2: Environment): if not environment_1: diff --git a/src/importspy/validators/variable_validator.py b/src/importspy/validators/variable_validator.py index 37fa2b6..3502c77 100644 --- a/src/importspy/validators/variable_validator.py +++ b/src/importspy/validators/variable_validator.py @@ -16,7 +16,6 @@ from importspy.log_manager import LogManager from importspy.models import Variable from importspy.constants import Constants -from importspy.contexts import Context class VariableValidator: @@ -27,24 +26,10 @@ class VariableValidator: - Presence (missing variables) - Value equality - Annotation correctness (for typed contexts) - - Attributes - ---------- - context : Context - A `Context` subclass instance that defines how to format errors and describe the scope. - logger : logging.Logger - Logger used for structured debug output. """ def __init__(self, context: Context): - """ - Initialize the validator with a context. - - Parameters - ---------- - context : Context - A context object that encapsulates the validation scope and error message logic. - """ + self.context = context self.logger = LogManager().get_logger(self.__class__.__name__) From b466f2773fec2b51197bbf605dca8d77e7241861 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Fri, 16 May 2025 15:03:22 +0200 Subject: [PATCH 05/40] refactor(validators): partial cleanup of legacy validator architecture - Removed obsolete validators (argument, attribute, function, module, system, python) - Began replacing static validation flow with context-aware logic - Introduced early wiring for runtime/module validation in Spy entrypoint - Stubbed modular validator orchestration using dedicated runtime/module classes - Temporary API degradation in function/module validation pending reimplementation Note: This commit breaks some validation logic. Work is paused mid-refactor. --- src/importspy/models.py | 5 +- src/importspy/s.py | 54 ++- src/importspy/validators.py | 363 ++++++++++++++++++ .../validators/function_validator.py | 100 ----- src/importspy/validators/module_validator.py | 139 ------- src/importspy/validators/python_validator.py | 135 ------- src/importspy/validators/runtime_validator.py | 82 ---- .../validators/spymodel_validator.py | 93 ----- src/importspy/validators/system_validator.py | 118 ------ .../validators/variable_validator.py | 112 ------ 10 files changed, 390 insertions(+), 811 deletions(-) create mode 100644 src/importspy/validators.py delete mode 100644 src/importspy/validators/function_validator.py delete mode 100644 src/importspy/validators/module_validator.py delete mode 100644 src/importspy/validators/python_validator.py delete mode 100644 src/importspy/validators/runtime_validator.py delete mode 100644 src/importspy/validators/spymodel_validator.py delete mode 100644 src/importspy/validators/system_validator.py delete mode 100644 src/importspy/validators/variable_validator.py diff --git a/src/importspy/models.py b/src/importspy/models.py index 68c4a1c..4435e73 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -18,6 +18,8 @@ Union ) +from abc import ABC + from types import ModuleType from .utilities.module_util import ( @@ -335,7 +337,8 @@ class Error: solution: str @classmethod - def from_template(cls, *, context: str, category: str, label: str, **kwargs): + def from_template(cls, *, context: str, category: str, **kwargs): + label = "" tpl = Errors.ERROR_MESSAGE_TEMPLATES.get(category) description = tpl[Errors.TEMPLATE_KEY].format(label=label, **kwargs) solution = tpl[Errors.SOLUTION_KEY] diff --git a/src/importspy/s.py b/src/importspy/s.py index b421685..2da4b37 100644 --- a/src/importspy/s.py +++ b/src/importspy/s.py @@ -20,12 +20,26 @@ """ from types import ModuleType -from .models import SpyModel +from .models import ( + SpyModel, + Runtime, + Python, + Module +) from .utilities.module_util import ModuleUtil -from .validators.spymodel_validator import SpyModelValidator +from .validators import ( + RuntimeValidator, + SystemValidator, + PythonValidator, + ModuleValidator + +) from .log_manager import LogManager from .persistences import Parser, YamlParser -from typing import Optional +from typing import ( + Optional, + List +) import logging @@ -134,38 +148,17 @@ def _configure_logging(self, log_level: Optional[int] = None): log_manager.configure(level=log_level or system_log_level) def _validate_module(self, spymodel: SpyModel, info_module: ModuleType) -> ModuleType: - """ - Compares a module's structure against the loaded import contract. - - This includes checking for: - - required classes and methods - - expected variable names and values - - inheritance and method signatures - - Parameters: - ----------- - spymodel : SpyModel - Parsed import contract used as the validation baseline. - - info_module : ModuleType - The actual module being validated. - - Returns: - -------- - ModuleType - The validated module. - - Raises: - ------- - ValueError - If the module does not conform to the expected structure. - """ self.logger.debug(f"info_module: {info_module}") if spymodel: + module_validator:ModuleValidator = ModuleValidator() self.logger.debug(f"Import contract detected: {spymodel}") spy_module = SpyModel.from_module(info_module) self.logger.debug(f"Extracted module structure: {spy_module}") - SpyModelValidator().validate(spymodel, spy_module) + module_validator.validate([spymodel],spy_module.deployments[0].systems[0].pythons[0].modules[0]) + runtime:Runtime = RuntimeValidator().validate(spymodel.deployments, spy_module.deployments) + pythons:List[Python] = SystemValidator().validate(runtime.systems, spy_module.deployments[0].systems) + modules: List[Module] = PythonValidator().validate(pythons, spy_module.deployments[0].systems[0].pythons) + module_validator.validate(modules, spy_module.deployments[0].systems[0].pythons[0].modules[0]) return ModuleUtil().load_module(info_module) def _inspect_module(self) -> ModuleType: @@ -190,7 +183,6 @@ def _inspect_module(self) -> ModuleType: current_frame, caller_frame = module_util.inspect_module() if current_frame.filename == caller_frame.filename: raise ValueError("Recursion detected during module analysis.") - info_module = module_util.get_info_module(caller_frame) self.logger.debug(f"Inferred caller module: {info_module}") return info_module diff --git a/src/importspy/validators.py b/src/importspy/validators.py new file mode 100644 index 0000000..06a236b --- /dev/null +++ b/src/importspy/validators.py @@ -0,0 +1,363 @@ +from typing import List +from .models import ( + Runtime, + System, + Environment, + Python, + Module +) + +from .constants import Constants + + +class RuntimeValidator: + + def validate( + self, + runtimes_1: List[Runtime], + runtimes_2: List[Runtime] + ): + if not runtimes_1: + return + + if not runtimes_2: + raise ValueError(Errors.ELEMENT_MISSING.format(runtimes_1)) + + runtime_2 = runtimes_2[0] + + for runtime_1 in runtimes_1: + if runtime_1.arch == runtime_2.arch: + return runtime_1 + +class SystemValidator: + + + def __init__(self): + + self._environment_validator = SystemValidator.EnvironmentValidator() + + def validate( + self, + systems_1: List[System], + systems_2: List[System] + ) -> None: + + if not systems_1: + return + + if not systems_2: + raise ValueError(Errors.ELEMENT_MISSING.format(systems_1)) + + system_2 = systems_2[0] + + for system_1 in systems_1: + if system_1.os == system_2.os: + if system_1.environment: + self._environment_validator.validate(system_1.environment, system_2.environment) + return system_1.pythons + + class EnvironmentValidator: + + def validate(self, + environment_1: Environment, + environment_2: Environment): + + if not environment_1: + return + + if not environment_2: + raise ValueError(Errors.ELEMENT_MISSING.format(environment_1)) + + variables_2 = environment_2.variables + + if environment_1.variables: + variables_1 = environment_1.variables + VariableValidator().validate(variables_1, variables_2) + return + +class PythonValidator: + + def validate( + self, + pythons_1: List[Python], + pythons_2: List[Python] + ): + if not pythons_1: + return + + if not pythons_2: + raise ValueError(Errors.ELEMENT_MISSING.format(pythons_1)) + + python_2 = pythons_2[0] + for python_1 in pythons_1: + + if self._is_python_match(python_1, python_2): + return python_1.modules + + def _is_python_match( + self, + python_1: Python, + python_2: Python + ) -> bool: + """ + Determine whether two Python configurations match. + + Parameters + ---------- + python_1 : Python + Expected configuration. + python_2 : Python + Actual system configuration. + + Returns + ------- + bool + `True` if the two configurations match according to the declared criteria, + otherwise `False`. + + Matching Criteria + ----------------- + - If both version and interpreter are defined: match both. + - If only version is defined: match version. + - If only interpreter is defined: match interpreter. + - If none are defined: match anything (default `True`). + """ + if python_1.version and python_1.interpreter: + return ( + python_1.version == python_2.version and + python_1.interpreter == python_2.interpreter + ) + + if python_1.version: + return python_1.version == python_2.version + + if python_1.interpreter: + return python_1.interpreter == python_2.interpreter + + return True + +class ModuleValidator: + + def validate( + self, + modules_1: List[Module], + module_2: Module + ): + if not modules_1: + return + + if not module_2: + raise ValueError(Errors.ELEMENT_MISSING.format(modules_1)) + + for module_1 in modules_1: + + if module_1.filename and module_1.filename != module_2.filename: + raise ValueError(Errors.FILENAME_MISMATCH.format(module_1.filename, module_2.filename)) + + if module_1.version and module_1.version != module_2.version: + raise ValueError(Errors.VERSION_MISMATCH.format(module_1.version, module_2.version)) + + self._variable_validator.validate( + module_1.variables, + module_2.variables) + + self._function_validator.validate( + module_1.functions, + module_2.functions + ) + + if module_1.classes: + for class_1 in module_1.classes: + class_2 = next((cls for cls in module_2.classes if cls.name == class_1.name), None) + if not class_2: + raise ValueError(Errors.CLASS_MISSING.format(class_1.name)) + + self._attribute_validator.validate( + class_1.attributes, + class_2.attributes, + class_1.name + ) + + self._function_validator.validate( + class_1.methods, + class_2.methods, + classname=class_1.name + ) + + CommonValidator().list_validate( + class_1.superclasses, + class_2.superclasses, + Errors.CLASS_SUPERCLASS_MISSING, + class_2.name + ) + return + +class VariableValidator: + + def __init__(self): + + self.logger = LogManager().get_logger(self.__class__.__name__) + + def validate( + self, + variables_1: List[Variable], + variables_2: List[Variable], + ): + """ + Validate two sets of variables for presence, value match, and type annotations. + + Parameters + ---------- + variables_1 : List[Variable] + The list of expected variables (from the contract). + variables_2 : List[Variable] + The list of actual variables (from the module/system). + + Raises + ------ + ValueError + If a variable is missing, has a mismatched value, or fails type annotation validation. + """ + self.logger.debug(f"Type of variables_1: {type(variables_1)}") + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Variable validating", + status="Starting", + details=f"Expected Variables: {variables_1} ; Actual Variables: {variables_2}" + ) + ) + + if not variables_1: + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Check if variables_1 is not none", + status="Finished", + details="No expected Variables to validate" + ) + ) + return + + if not variables_2: + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Checking variables_2 when variables_1 is missing", + status="Finished", + details="No actual Variables found for validation" + ) + ) + raise ValueError(self.context.format_missing_error()) + + for vars_1 in variables_1: + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Variable validating", + status="Progress", + details=f"Current vars_1: {vars_1}" + ) + ) + if vars_1.name not in {var.name for var in variables_2}: + raise ValueError(self.context.format_missing_error()) + + for vars_1 in variables_1: + vars_2 = next((var for var in variables_2 if var.name == vars_1.name), None) + if not vars_2: + raise ValueError(self.context.format_missing_error()) + + if vars_1.annotation and vars_1.annotation != vars_2.annotation: + raise ValueError( + self.context.format_mismatch_error(vars_1.annotation, vars_2.annotation) + ) + + if vars_1.value != vars_2.value: + raise ValueError( + self.context.format_mismatch_error(vars_1.value, vars_2.value) + ) + +class FunctionValidator: + + def __init__(self): + + self.logger = LogManager().get_logger(self.__class__.__name__) + + def validate( + self, + functions_1: List[Function], + functions_2: List[Function], + ): + + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Function validating", + status="Starting", + details=f"Expected functions: {functions_1} ; Current functions: {functions_2}" + ) + ) + + if not functions_1: + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Check if functions_1 is not none", + status="Finished", + details="No functions to validate" + ) + ) + return None + + if not functions_2: + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Checking functions_2 when functions_1 is missing", + status="Finished", + details="No actual functions found" + ) + ) + raise ValueError(Errors.ELEMENT_MISSING.format(functions_1)) + + for function_1 in functions_1: + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Function validating", + status="Progress", + details=f"Current function: {function_1}" + ) + ) + if function_1.name not in {f.name for f in functions_2}: + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Checking if function_1 is in functions_2", + status="Finished for function missing", + details=f"function_1: {function_1}; functions_2: {functions_2}" + ) + ) + raise ValueError( + Errors.ELEMENT_MISMATCH.format(context_name, function_1.name) + ) + + for function_1 in functions_1: + function_2 = next((f for f in functions_2 if f.name == function_1.name), None) + if not function_2: + raise ValueError(Errors.ELEMENT_MISSING.format(function_1)) + + self._argument_validator.validate( + function_1.arguments, + function_2.arguments, + function_1.name, + classname + ) + + if function_1.return_annotation and function_1.return_annotation != function_2.return_annotation: + raise ValueError( + Errors.FUNCTION_RETURN_ANNOTATION_MISMATCH.format( + function_1.name, + function_1.return_annotation, + function_2.return_annotation + ) + ) + + self.logger.debug( + Constants.LOG_MESSAGE_TEMPLATE.format( + operation="Function validating", + status="Completed", + details="Validation successful." + ) + ) \ No newline at end of file diff --git a/src/importspy/validators/function_validator.py b/src/importspy/validators/function_validator.py deleted file mode 100644 index a5c0fe5..0000000 --- a/src/importspy/validators/function_validator.py +++ /dev/null @@ -1,100 +0,0 @@ -from ..models import Function -from ..errors import Errors -from ..constants import Constants -from typing import List, Optional -from ..log_manager import LogManager -from .variable_validator import VariableValidator -from ..contexts import ( - Context, - MethodArgumentContext -) - - -class FunctionValidator: - - def __init__(self): - - self._variable_validator = VariableValidator(context=current_context) - - def validate( - self, - functions_1: List[Function], - functions_2: List[Function], - ) -> Optional[bool]: - - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Function validating", - status="Starting", - details=f"Expected functions: {functions_1} ; Current functions: {functions_2}" - ) - ) - - if not functions_1: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Check if functions_1 is not none", - status="Finished", - details="No functions to validate" - ) - ) - return None - - if not functions_2: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Checking functions_2 when functions_1 is missing", - status="Finished", - details="No actual functions found" - ) - ) - raise ValueError(Errors.ELEMENT_MISSING.format(functions_1)) - - for function_1 in functions_1: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Function validating", - status="Progress", - details=f"Current function: {function_1}" - ) - ) - if function_1.name not in {f.name for f in functions_2}: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Checking if function_1 is in functions_2", - status="Finished for function missing", - details=f"function_1: {function_1}; functions_2: {functions_2}" - ) - ) - raise ValueError( - Errors.ELEMENT_MISMATCH.format(context_name, function_1.name) - ) - - for function_1 in functions_1: - function_2 = next((f for f in functions_2 if f.name == function_1.name), None) - if not function_2: - raise ValueError(Errors.ELEMENT_MISSING.format(function_1)) - - self._argument_validator.validate( - function_1.arguments, - function_2.arguments, - function_1.name, - classname - ) - - if function_1.return_annotation and function_1.return_annotation != function_2.return_annotation: - raise ValueError( - Errors.FUNCTION_RETURN_ANNOTATION_MISMATCH.format( - function_1.name, - function_1.return_annotation, - function_2.return_annotation - ) - ) - - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Function validating", - status="Completed", - details="Validation successful." - ) - ) diff --git a/src/importspy/validators/module_validator.py b/src/importspy/validators/module_validator.py deleted file mode 100644 index 64cd96d..0000000 --- a/src/importspy/validators/module_validator.py +++ /dev/null @@ -1,139 +0,0 @@ -""" -importspy.validators.module_validator -===================================== - -Validator for Python modules and their internal structures. - -This module provides the `ModuleValidator` class, responsible for checking -that a Python module conforms to the expectations defined in an import contract. - -It validates: -- Module filename and version -- Declared global variables -- Top-level functions -- Declared classes, including their attributes, methods, and superclasses - -Delegates detailed checks to: -- `AttributeValidator` -- `FunctionValidator` -- `CommonValidator` -""" - -from ..models import Module -from ..errors import Errors -from .variable_validator import VariableValidator -from .function_validator import FunctionValidator -from typing import List, Optional - - -class ModuleValidator: - """ - Validator for full Python module metadata and structure. - - Attributes - ---------- - _attribute_validator : AttributeValidator - Responsible for validating class attributes. - _function_validator : FunctionValidator - Validates top-level and class methods. - """ - - def __init__(self): - """ - Initialize the validator with attribute and function checkers. - """ - self._variable_validator = VariableValidator() - self._attribute_validator = AttributeValidator() - self._function_validator = FunctionValidator() - - def validate( - self, - modules_1: List[Module], - module_2: Optional[Module] - ) -> Optional[None]: - """ - Validate one or more expected modules against the actual loaded module. - - Parameters - ---------- - modules_1 : List[Module] - List of expected module definitions from the import contract. - module_2 : Optional[Module] - The actual module extracted from the system for validation. - - Returns - ------- - None - Returns None when: - - No modules to validate (`modules_1` is empty). - - Validation is successful. - - Raises - ------ - ValueError - Raised if: - - `module_2` is missing. - - Filename or version mismatches. - - Variables differ in name or value. - - Missing or invalid functions, classes, attributes, or superclasses. - - Example - ------- - >>> validator = ModuleValidator() - >>> validator.validate([expected_module], actual_module) - """ - if not modules_1: - return - - if not module_2: - raise ValueError(Errors.ELEMENT_MISSING.format(modules_1)) - - for module_1 in modules_1: - # Check filename - if module_1.filename and module_1.filename != module_2.filename: - raise ValueError(Errors.FILENAME_MISMATCH.format(module_1.filename, module_2.filename)) - - # Check version - if module_1.version and module_1.version != module_2.version: - raise ValueError(Errors.VERSION_MISMATCH.format(module_1.version, module_2.version)) - - self._variable_validator.validate( - module_1.variables, - module_2.variables) - - # Validate top-level functions - self._function_validator.validate( - module_1.functions, - module_2.functions - ) - - # Validate classes and class contents - if module_1.classes: - for class_1 in module_1.classes: - class_2 = next((cls for cls in module_2.classes if cls.name == class_1.name), None) - if not class_2: - raise ValueError(Errors.CLASS_MISSING.format(class_1.name)) - - # Class attribute check - self._attribute_validator.validate( - class_1.attributes, - class_2.attributes, - class_1.name - ) - - # Method (function) check - self._function_validator.validate( - class_1.methods, - class_2.methods, - classname=class_1.name - ) - - # Superclass check - CommonValidator().list_validate( - class_1.superclasses, - class_2.superclasses, - Errors.CLASS_SUPERCLASS_MISSING, - class_2.name - ) - - return diff --git a/src/importspy/validators/python_validator.py b/src/importspy/validators/python_validator.py deleted file mode 100644 index b6cc131..0000000 --- a/src/importspy/validators/python_validator.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -importspy.validators.python_validator -===================================== - -Validator for Python runtime configurations. - -This module defines the `PythonValidator` class, responsible for validating -the Python version, interpreter, and associated modules declared in an -import contract against the actual Python runtime context. - -Delegates module-level validation to `ModuleValidator`. -""" - -from ..models import Python -from ..errors import Errors -from .module_validator import ModuleValidator -from ..log_manager import LogManager -from ..constants import Constants -from typing import List, Optional - - -class PythonValidator: - """ - Validates Python runtime configuration and associated modules. - - Attributes - ---------- - logger : logging.Logger - Logger instance for debugging and tracing. - _module_validator : ModuleValidator - Validator for modules within the Python configuration. - """ - - def __init__(self): - """ - Initialize the validator and internal module validator. - """ - self.logger = LogManager().get_logger(self.__class__.__name__) - self._module_validator = ModuleValidator() - - def validate( - self, - pythons_1: List[Python], - pythons_2: Optional[List[Python]] - ) -> Optional[None]: - """ - Validate a list of expected Python environments against actual ones. - - Parameters - ---------- - pythons_1 : List[Python] - Expected Python configurations from the contract. - pythons_2 : Optional[List[Python]] - Actual Python runtime details extracted from the system. - - Returns - ------- - None - Returned when: - - `pythons_1` is empty (no validation needed). - - Validation succeeds. - - Raises - ------ - ValueError - If `pythons_2` is missing or does not match - the declared expectations in `pythons_1`. - - Example - ------- - >>> validator = PythonValidator() - >>> validator.validate([expected_python], [actual_python]) - """ - if not pythons_1: - return - - if not pythons_2: - raise ValueError(Errors.ELEMENT_MISSING.format(pythons_1)) - - python_2 = pythons_2[0] - for python_1 in pythons_1: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Python validating", - status="Progress", - details=f"Expected python: {python_1} ; Current python: {python_2}" - ) - ) - - if self._is_python_match(python_1, python_2): - if python_2.modules: - self._module_validator.validate(python_1.modules, python_2.modules[0]) - return - - def _is_python_match( - self, - python_1: Python, - python_2: Python - ) -> bool: - """ - Determine whether two Python configurations match. - - Parameters - ---------- - python_1 : Python - Expected configuration. - python_2 : Python - Actual system configuration. - - Returns - ------- - bool - `True` if the two configurations match according to the declared criteria, - otherwise `False`. - - Matching Criteria - ----------------- - - If both version and interpreter are defined: match both. - - If only version is defined: match version. - - If only interpreter is defined: match interpreter. - - If none are defined: match anything (default `True`). - """ - if python_1.version and python_1.interpreter: - return ( - python_1.version == python_2.version and - python_1.interpreter == python_2.interpreter - ) - - if python_1.version: - return python_1.version == python_2.version - - if python_1.interpreter: - return python_1.interpreter == python_2.interpreter - - return True diff --git a/src/importspy/validators/runtime_validator.py b/src/importspy/validators/runtime_validator.py deleted file mode 100644 index 87e0ff3..0000000 --- a/src/importspy/validators/runtime_validator.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -importspy.validators.runtime_validator -====================================== - -Validator for runtime configurations. - -This module defines the `RuntimeValidator` class, which ensures that the -runtime architecture and system-level environment of a Python module -conform to what is declared in its import contract. - -Delegates system validation to `SystemValidator`. -""" - -from ..models import Runtime -from ..errors import Errors -from .system_validator import SystemValidator -from typing import List - - -class RuntimeValidator: - """ - Validates runtime architecture and system configurations. - - Attributes - ---------- - _system_validator : SystemValidator - Handles validation of OS and platform-specific system expectations. - """ - - def __init__(self): - """ - Initialize the runtime validator and prepare the system validator. - """ - self._system_validator = SystemValidator() - - def validate( - self, - runtimes_1: List[Runtime], - runtimes_2: List[Runtime] - ) -> None: - """ - Validate expected runtime declarations against actual runtime data. - - Parameters - ---------- - runtimes_1 : List[Runtime] - The expected runtime environments declared in the contract. - runtimes_2 : List[Runtime] - The actual detected runtime environments from the host system. - - Returns - ------- - None - Returned when: - - `runtimes_1` is empty (no validation required). - - Validation completes successfully. - - Raises - ------ - ValueError - - If `runtimes_2` is missing but expectations are defined. - - If the architectures do not match. - - If any contained system-level configuration mismatches are detected. - - Example - ------- - >>> validator = RuntimeValidator() - >>> validator.validate([expected_runtime], [actual_runtime]) - """ - if not runtimes_1: - return - - if not runtimes_2: - raise ValueError(Errors.ELEMENT_MISSING.format(runtimes_1)) - - runtime_2 = runtimes_2[0] - - for runtime_1 in runtimes_1: - if runtime_1.arch == runtime_2.arch: - if runtime_1.systems: - self._system_validator.validate(runtime_1.systems, runtime_2.systems) - return diff --git a/src/importspy/validators/spymodel_validator.py b/src/importspy/validators/spymodel_validator.py deleted file mode 100644 index fb57be8..0000000 --- a/src/importspy/validators/spymodel_validator.py +++ /dev/null @@ -1,93 +0,0 @@ -""" -importspy.validators.spymodel_validator -======================================== - -Validator for top-level SpyModel objects. - -This module defines the `SpyModelValidator` class, which orchestrates full-model validation, -including both runtime environments and declared modules. It is typically invoked as the -final step during the ImportSpy validation pipeline. -""" - -from ..models import SpyModel -from .runtime_validator import RuntimeValidator -from .module_validator import ModuleValidator - - -class SpyModelValidator: - """ - Validates the full structure of an ImportSpy model contract. - - The `SpyModelValidator` ensures that: - - Declared runtime deployments (architecture + system + interpreter) match. - - Declared module definitions (files, classes, functions) match. - - Delegates: - ---------- - - Runtime inspection to `RuntimeValidator` - - Module structure comparison to `ModuleValidator` - - Validation Scope: - ----------------- - ✓ Architecture and OS validation - ✓ Interpreter and Python version match - ✓ Module filename, version, structure, functions, classes - - This validator serves as the **entry point** for verifying SpyModel objects, - typically loaded from YAML contracts and dynamically matched against live modules. - - Attributes - ---------- - _runtime_validator : RuntimeValidator - Validates runtime architecture and interpreter. - _module_validator : ModuleValidator - Validates classes, functions, and variables inside modules. - """ - - def __init__(self): - """ - Initializes the SpyModelValidator with supporting sub-validators. - """ - self._runtime_validator = RuntimeValidator() - self._module_validator = ModuleValidator() - - def validate( - self, - spy_model_1: SpyModel, - spy_model_2: SpyModel - ) -> None: - """ - Validates a declared SpyModel (from contract) against the active runtime SpyModel. - - Parameters - ---------- - spy_model_1 : SpyModel - The expected SpyModel structure (loaded from contract). - spy_model_2 : SpyModel - The actual SpyModel structure (derived from live inspection). - - Returns - ------- - None - If validation passes or `spy_model_1` has no runtime deployments to validate. - - Raises - ------ - ValueError - If architecture, interpreter, modules, or structural expectations are not met. - - Example - ------- - >>> validator = SpyModelValidator() - >>> validator.validate(spy_model_contract, spy_model_live) - """ - self._runtime_validator.validate(spy_model_1.deployments, spy_model_2.deployments) - - self._module_validator.validate( - [spy_model_1], - spy_model_2 - .deployments[0] - .systems[0] - .pythons[0] - .modules[0] - ) diff --git a/src/importspy/validators/system_validator.py b/src/importspy/validators/system_validator.py deleted file mode 100644 index e94b168..0000000 --- a/src/importspy/validators/system_validator.py +++ /dev/null @@ -1,118 +0,0 @@ -""" -importspy.validators.system_validator -====================================== - -Validator for system-level configurations. - -This module defines the `SystemValidator` class, responsible for validating -operating systems, environment variables, and Python interpreter settings -within a runtime context. - -Delegates: -- Environment and key-value matching to `CommonValidator` -- Python version/interpreter matching to `PythonValidator` -""" - -from typing import List -from ..models import ( - System, - Environment -) -from ..errors import Errors -from .python_validator import PythonValidator -from .variable_validator import VariableValidator - -class SystemValidator: - """ - Validates system-level execution environments. - - This includes: - - Operating system matching - - Environment variable validation - - Python configuration checks (via `PythonValidator`) - - Attributes - ---------- - _python_validator : PythonValidator - Handles validation of nested Python interpreter configurations. - """ - - def __init__(self): - """ - Initialize the system validator and prepare supporting validators. - """ - self._python_validator = PythonValidator() - self._environment_validator = SystemValidator.EnvironmentValidator() - - def validate( - self, - systems_1: List[System], - systems_2: List[System] - ) -> None: - """ - Validate declared system expectations against actual system properties. - - Parameters - ---------- - systems_1 : List[System] - Expected system configurations as declared in the import contract. - systems_2 : List[System] - Actual detected system environment. - - Returns - ------- - None - Returned when: - - `systems_1` is empty (no validation required). - - Validation completes successfully without mismatches. - - Raises - ------ - ValueError - - If `systems_2` is missing but expected. - - If operating systems do not match. - - If environment variables mismatch or are missing. - - If any Python interpreter configuration fails validation. - - Example - ------- - >>> validator = SystemValidator() - >>> validator.validate([expected_system], [actual_system]) - """ - if not systems_1: - return - - if not systems_2: - raise ValueError(Errors.ELEMENT_MISSING.format(systems_1)) - - system_2 = systems_2[0] - - for system_1 in systems_1: - if system_1.os == system_2.os: - if system_1.environment: - self._environment_validator.validate(system_1.environment, system_2.environment) - if system_1.pythons: - self._python_validator.validate(system_1.pythons, system_2.pythons) - return - - class EnvironmentValidator: - - def __init__(self): - self._variable_validator = VariableValidator(context="The environment variable") - - def validate(self, - environment_1: Environment, - environment_2: Environment): - - if not environment_1: - return - - if not environment_2: - raise ValueError(Errors.ELEMENT_MISSING.format(environment_1)) - - variables_2 = environment_2.variables - - if environment_1.variables: - variables_1 = environment_1.variables - self._variable_validator.validate(variables_1, variables_2) - return \ No newline at end of file diff --git a/src/importspy/validators/variable_validator.py b/src/importspy/validators/variable_validator.py deleted file mode 100644 index 3502c77..0000000 --- a/src/importspy/validators/variable_validator.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -importspy.validators.variable_validator -======================================= - -This module provides validation logic for variables, including standalone variables, -function arguments, class attributes, and environment variables. - -Each validation runs in the context of a specific `Context` subclass, which encapsulates -label rendering and scoped error formatting for missing, mismatched, or invalid elements. - -Author: ImportSpy Team -License: MIT -""" - -from typing import List -from importspy.log_manager import LogManager -from importspy.models import Variable -from importspy.constants import Constants - - -class VariableValidator: - """ - Validates lists of `Variable` instances within a given scope-aware context. - - This class compares expected and actual variables for: - - Presence (missing variables) - - Value equality - - Annotation correctness (for typed contexts) - """ - - def __init__(self, context: Context): - - self.context = context - self.logger = LogManager().get_logger(self.__class__.__name__) - - def validate( - self, - variables_1: List[Variable], - variables_2: List[Variable], - ): - """ - Validate two sets of variables for presence, value match, and type annotations. - - Parameters - ---------- - variables_1 : List[Variable] - The list of expected variables (from the contract). - variables_2 : List[Variable] - The list of actual variables (from the module/system). - - Raises - ------ - ValueError - If a variable is missing, has a mismatched value, or fails type annotation validation. - """ - self.logger.debug(f"Context: {self.context.__class__.__name__} (scope={self.context.scope})") - self.logger.debug(f"Type of variables_1: {type(variables_1)}") - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Variable validating", - status="Starting", - details=f"Expected Variables: {variables_1} ; Actual Variables: {variables_2}" - ) - ) - - if not variables_1: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Check if variables_1 is not none", - status="Finished", - details="No expected Variables to validate" - ) - ) - return - - if not variables_2: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Checking variables_2 when variables_1 is missing", - status="Finished", - details="No actual Variables found for validation" - ) - ) - raise ValueError(self.context.format_missing_error()) - - for vars_1 in variables_1: - self.logger.debug( - Constants.LOG_MESSAGE_TEMPLATE.format( - operation="Variable validating", - status="Progress", - details=f"Current vars_1: {vars_1}" - ) - ) - if vars_1.name not in {var.name for var in variables_2}: - raise ValueError(self.context.format_missing_error()) - - for vars_1 in variables_1: - vars_2 = next((var for var in variables_2 if var.name == vars_1.name), None) - if not vars_2: - raise ValueError(self.context.format_missing_error()) - - # Annotation mismatch (only if not validating env vars) - if self.context.scope != Constants.SCOPE_ENVIRONMENT and vars_1.annotation and vars_1.annotation != vars_2.annotation: - raise ValueError( - self.context.format_mismatch_error(vars_1.annotation, vars_2.annotation) - ) - - # Value mismatch - if vars_1.value != vars_2.value: - raise ValueError( - self.context.format_mismatch_error(vars_1.value, vars_2.value) - ) From 6a5b56a24647d1d63dbfbebb6d19dcb8725814be Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Sat, 17 May 2025 18:18:29 +0200 Subject: [PATCH 06/40] feat(errors): introduce structured contract violation system for label-based error formatting - Renamed error label templates to VARIABLES_LABEL_TEMPLATE and FUNCTIONS_LABEL_TEMPLATE for clarity. - Introduced abstract base class `ContractViolation` and concrete implementations: - VariableContractViolation - FunctionContractViolation - Added dataclass-based bundles for different scopes (ClassBundle, ModuleBundle, EnvironmentBundle). - Refactored `Error` model to support instantiation from a `ContractViolation` using `from_contract_violation`. - Label generation is now context-aware and dynamically formatted from scope-specific bundles. This refactor prepares the foundation for more granular and serializable error reporting in ImportSpy. --- src/importspy/constants.py | 18 +++---- src/importspy/models.py | 100 +++++++++++++++++++++++++++++++++---- 2 files changed, 100 insertions(+), 18 deletions(-) diff --git a/src/importspy/constants.py b/src/importspy/constants.py index 8058966..928f918 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -161,22 +161,22 @@ class Category(str, Enum): MISMATCH = "mismatch" INVALID = "invalid" - VARIABLES_TEMPLATE = { + VARIABLES_LABEL_TEMPLATE = { SCOPE_VARIABLE: { - Contexts.ENVIRONMENT_CONTEXT: 'The environment variable "{name}"', - Contexts.MODULE_CONTEXT: 'The variable "{name}" in module "{module}"', - Contexts.CLASS_CONTEXT: 'The {attribute_type} attribute "{name}" in class "{class_name}"' + Contexts.ENVIRONMENT_CONTEXT: 'The environment variable "{environment_variable_name}"', + Contexts.MODULE_CONTEXT: 'The variable "{variable_name}" in module "{module_name}"', + Contexts.CLASS_CONTEXT: 'The {attribute_type} attribute "{attribute_name}" in class "{class_name}"' }, SCOPE_ARGUMENT: { - Contexts.MODULE_CONTEXT: 'The argument "{name}" of function "{function_name}"', - Contexts.CLASS_CONTEXT: 'The argument "{name}" of method "{method_name}" in class "{class_name}"', + Contexts.MODULE_CONTEXT: 'The argument "{argument_name}" of function "{function_name}"', + Contexts.CLASS_CONTEXT: 'The argument "{argument_name}" of method "{method_name}" in class "{class_name}"', } } - FUNCTIONS_TEMPLATE = { - Contexts.MODULE_CONTEXT: 'The function "{name}" in module "{module}"', - Contexts.CLASS_CONTEXT: 'The method "{name}" in class "{class_name}"' + FUNCTIONS_LABEL_TEMPLATE = { + Contexts.MODULE_CONTEXT: 'The function "{function_name}" in module "{module_name}"', + Contexts.CLASS_CONTEXT: 'The method "{method_name}" in class "{class_name}"' } ERROR_MESSAGE_TEMPLATES = { diff --git a/src/importspy/models.py b/src/importspy/models.py index 4435e73..7641142 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -18,7 +18,12 @@ Union ) -from abc import ABC +from abc import ( + ABC, + abstractmethod +) + +from dataclasses import dataclass, asdict from types import ModuleType @@ -328,23 +333,24 @@ def from_module(cls, info_module: ModuleType): ] ) -class Error: +class Error(BaseModel): context: Contexts - title: str = Errors.CONTEXT_INTRO.get(context) + title: str category: Errors.Category description: str solution: str @classmethod - def from_template(cls, *, context: str, category: str, **kwargs): - label = "" - tpl = Errors.ERROR_MESSAGE_TEMPLATES.get(category) - description = tpl[Errors.TEMPLATE_KEY].format(label=label, **kwargs) + def from_contract_violation(cls, contract_violation: 'ContractViolation'): + tpl = Errors.ERROR_MESSAGE_TEMPLATES.get(contract_violation.category) + title = Errors.CONTEXT_INTRO.get(contract_violation.context) + description = tpl[Errors.TEMPLATE_KEY].format(label=contract_violation.label) solution = tpl[Errors.SOLUTION_KEY] return cls( - context=context, - category=category, + context=contract_violation.context, + title=title, + category=contract_violation.category, description=description, solution=solution ) @@ -352,5 +358,81 @@ def from_template(cls, *, context: str, category: str, **kwargs): def render_message(self) -> str: return f"[{self.title}] {self.description} {self.solution}" +class ContractViolation(ABC): + + @abstractmethod + @property + def context(self) -> str: + pass + + @abstractmethod + @property + def category(self) -> str: + pass + + @abstractmethod + @property + def label(self) -> str: + pass + +class BaseContractViolation(ContractViolation): + + def __init__(self, context, category): + + self.context = context + self.category = category + super().__init__() + + @property + def context(self) -> str: + return self.context + + @property + def category(self) -> str: + return self.category + +class VariableContractViolation(BaseContractViolation): + + def __init__(self, scope:str, context:str, category:str, bundle:Union['ModuleBundle' ,'ClassBundle', 'EnvironmentBundle']): + super().__init__(context, category) + self.scope = scope + self.bundle = bundle + + @property + def label(self) -> str: + return Errors.VARIABLES_LABEL_TEMPLATE[self.scope][self.context].format(**asdict(self.bundle)) + +class FunctionContractViolation(BaseContractViolation): + + def __init__(self, context:str, category:str, bundle:Union['ClassBundle']): + super().__init__(context, category) + self.bundle = bundle + + @property + def label(self) -> str: + return Errors.FUNCTIONS_LABEL_TEMPLATE[self.context].format(**asdict(self.bundle)) + +@dataclass +class ClassBundle: + + class_name: Optional[str] = None + attribute_type: Optional[str] = None + attribute_name: Optional[str] = None + argument_name: Optional[str] = None + method_name: Optional[str] = None + +@dataclass +class ModuleBundle: + + variable_name: Optional[str] = None + module_name: Optional[str] = None + argument_name: Optional[str] = None + function_name: Optional[str] = None + +@dataclass +class EnvironmentBundle: + + environment_variable_name: Optional[str] = None + From e0c0fb1e2e43f61249594decb89041507d8b6cde Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Sun, 18 May 2025 12:25:05 +0200 Subject: [PATCH 07/40] refactor(constants, models): replace literals with typed enums for validation constraints - Introduced Enum-based classes in `Constants` for supported architectures, OS, Python versions, interpreters, annotations, and class attribute types. - Updated `Runtime`, `System`, `Python`, `Variable`, `Attribute`, and `Function` models to use strongly typed enums. - Removed Pydantic field validators in favor of stricter enum typing. - Extended `BaseContractViolation` with error rendering methods for missing, mismatch, and invalid violations. - Adjusted Python version extraction to return only major.minor format. This refactor improves type safety, removes redundant runtime checks, and paves the way for better validation consistency. --- src/importspy/constants.py | 115 +++++++--------------- src/importspy/models.py | 127 ++++++++----------------- src/importspy/utilities/python_util.py | 5 +- 3 files changed, 79 insertions(+), 168 deletions(-) diff --git a/src/importspy/constants.py b/src/importspy/constants.py index 928f918..6c04a9a 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -13,93 +13,51 @@ class Constants: Unlike `Config`, which defines values dynamically from the runtime or user environment, `Constants` serves as the fixed baseline for what ImportSpy considers valid and contract-compliant. - - Attributes: - KNOWN_ARCHITECTURES (List[str]): - List of CPU architectures supported in runtime validation, - including 'x86_64', 'arm64', 'i386', and others. - - SUPPORTED_OS (List[str]): - List of supported operating systems: 'linux', 'windows', and 'darwin'. - - SUPPORTED_PYTHON_VERSION (List[str]): - List of supported Python versions, e.g. '3.9', '3.10', '3.11', etc. - - SUPPORTED_PYTHON_IMPLEMENTATION (List[str]): - Python interpreter implementations recognized by ImportSpy, - such as 'CPython', 'PyPy', 'IronPython', and others. - - SUPPORTED_CLASS_ATTRIBUTE_TYPES (List[str]): - Allowed attribute type classifications: 'class' and 'instance'. - - SUPPORTED_ANNOTATIONS (List[str]): - Allowed annotation types used for validating variables, - arguments, and return values. Includes types such as - 'int', 'str', 'Optional', 'Union', 'Callable', etc. - - NAME (str): - Metadata key used for referencing object names in the model. - - VALUE (str): - Metadata key used to represent literal values in contracts. - - ANNOTATION (str): - Metadata key used to refer to a declared annotation in contracts. - - CLASS_TYPE (str): - String literal used to label a class-level attribute type. - - INSTANCE_TYPE (str): - String literal used to label an instance-level attribute type. - - LOG_MESSAGE_TEMPLATE (str): - Template string for standardized log message formatting - during contract evaluation and model parsing. """ - KNOWN_ARCHITECTURES = [ - Config.ARCH_x86_64, - Config.ARCH_AARCH64, - Config.ARCH_ARM, - Config.ARCH_ARM64, - Config.ARCH_I386, - Config.ARCH_PPC64, - Config.ARCH_PPC64LE, + class SupportedArchitectures: + + Config.ARCH_x86_64 + Config.ARCH_AARCH64 + Config.ARCH_ARM + Config.ARCH_ARM64 + Config.ARCH_I386 + Config.ARCH_PPC64 + Config.ARCH_PPC64LE Config.ARCH_S390X - ] - SUPPORTED_OS = [ - Config.OS_WINDOWS, - Config.OS_LINUX, + class SupportedOS: + + Config.OS_WINDOWS + Config.OS_LINUX Config.OS_MACOS - ] - SUPPORTED_PYTHON_VERSION=[ - Config.PYTHON_VERSION_3_13, - Config.PYTHON_VERSION_3_12, - Config.PYTHON_VERSION_3_11, - Config.PYTHON_VERSION_3_10, + class SupportedPythonVersions(str, Enum): + + Config.PYTHON_VERSION_3_13 + Config.PYTHON_VERSION_3_12 + Config.PYTHON_VERSION_3_11 + Config.PYTHON_VERSION_3_10 Config.PYTHON_VERSION_3_9 - ] - - SUPPORTED_PYTHON_IMPLEMENTATION = [ - Config.INTERPRETER_CPYTHON, - Config.INTERPRETER_PYPY, - Config.INTERPRETER_JYTHON, - Config.INTERPRETER_IRON_PYTHON, - Config.INTERPRETER_MICROPYTHON, - Config.INTERPRETER_BRYTHON, - Config.INTERPRETER_PYSTON, - Config.INTERPRETER_GRAALPYTHON, - Config.INTERPRETER_RUSTPYTHON, - Config.INTERPRETER_NUITKA, + + class SupportedPythonImplementations(str, Enum): + + Config.INTERPRETER_CPYTHON + Config.INTERPRETER_PYPY + Config.INTERPRETER_JYTHON + Config.INTERPRETER_IRON_PYTHON + Config.INTERPRETER_MICROPYTHON + Config.INTERPRETER_BRYTHON + Config.INTERPRETER_PYSTON + Config.INTERPRETER_GRAALPYTHON + Config.INTERPRETER_RUSTPYTHON + Config.INTERPRETER_NUITKA Config.INTERPRETER_TRANSCRYPT - ] - SUPPORTED_CLASS_ATTRIBUTE_TYPES = [ - Config.CLASS_TYPE, + class SupportedClassAttributeTypes(str, Enum): + + Config.CLASS_TYPE Config.INSTANCE_TYPE - ] NAME = "Name" VALUE = "Value" @@ -108,7 +66,7 @@ class Constants: CLASS_TYPE = Config.CLASS_TYPE INSTANCE_TYPE = Config.INSTANCE_TYPE - SUPPORTED_ANNOTATIONS = [ + class SupportedAnnotations(str, Enum): Config.ANNOTATION_INT, Config.ANNOTATION_FLOAT, Config.ANNOTATION_STR, @@ -124,7 +82,6 @@ class Constants: Config.ANNOTATION_LIST, Config.ANNOTATION_DICT, Config.ANNOTATION_TUPLE - ] LOG_MESSAGE_TEMPLATE = ( "[Operation: {operation}] [Status: {status}] " diff --git a/src/importspy/models.py b/src/importspy/models.py index 7641142..d825a66 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -8,14 +8,12 @@ system-level metadata required to enforce import contracts across execution contexts. """ -from pydantic import ( - BaseModel, - field_validator -) +from pydantic import BaseModel from typing import ( Optional, - Union + Union, + Any ) from abc import ( @@ -52,28 +50,10 @@ class Python(BaseModel): Includes the Python version, interpreter type, and the list of loaded modules. Used to validate compatibility between caller and callee environments. """ - version: Optional[str] = None - interpreter: Optional[str] = None + version: Optional[Constants.SupportedPythonVersions] = None + interpreter: Optional[Constants.SupportedPythonImplementations] = None modules: list['Module'] - @field_validator('version') - def validate_version(cls, value: str): - """ - Validate that the Python version is within supported versions. - """ - if ".".join(value.split(".")[:2]) not in Constants.SUPPORTED_PYTHON_VERSION: - raise ValueError(Errors.INVALID_PYTHON_VERSION.format(Constants.SUPPORTED_PYTHON_VERSION, value)) - return value - - @field_validator('interpreter') - def validate_interpreter(cls, value: str): - """ - Validate that the interpreter is among the supported Python implementations. - """ - if value not in Constants.SUPPORTED_PYTHON_IMPLEMENTATION: - raise ValueError(Errors.INVALID_PYTHON_INTERPRETER.format(Constants.SUPPORTED_PYTHON_IMPLEMENTATION, value)) - return value - class Environment(BaseModel): """ Represents a set of environment variables and secret keys @@ -87,37 +67,19 @@ class System(BaseModel): Represents the system environment, including OS, environment variables, and Python runtimes configured within the system. """ - os: str + os: Constants.SupportedOS environment: Optional[Environment] = None pythons: list[Python] - @field_validator('os') - def validate_os(cls, value: str): - """ - Validate that the provided OS is among the supported platforms. - """ - if value not in Constants.SUPPORTED_OS: - raise ValueError(Errors.INVALID_OS.format(Constants.SUPPORTED_OS, value)) - return value - class Runtime(BaseModel): """ Represents the deployment runtime, identified by CPU architecture and the list of supported systems associated with that architecture. """ - arch: str + arch: Constants.SupportedArchitectures systems: list[System] - @field_validator('arch') - def validate_arch(cls, value: str): - """ - Validate that the CPU architecture is known and supported. - """ - if value not in Constants.KNOWN_ARCHITECTURES: - raise ValueError(Errors.INVALID_ARCHITECTURE.format(value, Constants.KNOWN_ARCHITECTURES)) - return value - class Variable(BaseModel): """ @@ -125,23 +87,9 @@ class Variable(BaseModel): annotation and value. Used for structural validation of the importing module. """ name: str - annotation: Optional[str] = None + annotation: Optional[Constants.SupportedAnnotations] = None value: Optional[Union[int, str, float, bool, None]] = None - @field_validator("annotation") - def validate_annotation(cls, value): - """ - Validate that the annotation is supported by the current contract. - """ - if not value: - return None - base = value.split("[")[0] - if base not in Constants.SUPPORTED_ANNOTATIONS: - raise ValueError( - Errors.INVALID_ANNOTATION.format(value, Constants.SUPPORTED_ANNOTATIONS) - ) - return value - @classmethod def from_variable_info(cls, variables_info: list[VariableInfo]): """ @@ -159,16 +107,7 @@ class Attribute(Variable): Represents a class attribute, extending Variable with a 'type' indicator (e.g., 'class', 'instance'). Used in class-level contract validation. """ - type: str - - @field_validator('type') - def validate_type(cls, value: str): - """ - Validate that the attribute type is among supported class attribute types. - """ - if value not in Constants.SUPPORTED_CLASS_ATTRIBUTE_TYPES: - raise ValueError(Errors.INVALID_ATTRIBUTE_TYPE.format(value, Constants.SUPPORTED_CLASS_ATTRIBUTE_TYPES)) - return value + type: Constants.SupportedClassAttributeTypes @classmethod def from_attributes_info(cls, attributes_info: list[AttributeInfo]): @@ -207,14 +146,7 @@ class Function(BaseModel): """ name: str arguments: Optional[list[Argument]] = None - return_annotation: Optional[str] = None - - @field_validator("return_annotation") - def validate_annotation(cls, value): - """ - Validate that the return annotation is supported. - """ - return CommonValidator.validate_annotation(value) + return_annotation: Optional[Constants.SupportedAnnotations] = None @classmethod def from_functions_info(cls, functions_info: list[FunctionInfo]): @@ -360,36 +292,57 @@ def render_message(self) -> str: class ContractViolation(ABC): - @abstractmethod @property + @abstractmethod def context(self) -> str: pass - - @abstractmethod + @property + @abstractmethod def category(self) -> str: pass - - @abstractmethod + @property + @abstractmethod def label(self) -> str: pass + + @abstractmethod + def missing_error_handler(self) -> str: + pass + + @abstractmethod + def mismatch_error_handler(self) -> str: + pass + + @abstractmethod + def invalid_error_handler(self) -> str: + pass class BaseContractViolation(ContractViolation): def __init__(self, context, category): - self.context = context - self.category = category + self._context = context + self._category = category super().__init__() @property def context(self) -> str: - return self.context + return self._context @property def category(self) -> str: - return self.category + return self._category + + def missing_error_handler(self) -> str: + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label).capitalize()}' + + def mismatch_error_handler(self, expected:Any, actual:Any) -> str: + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label, expected=expected, actual=actual).capitalize()}' + + def invalid_error_handler(self, allowed:Any, found:Any) -> str: + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label, expected=allowed, actual=found).capitalize()}' class VariableContractViolation(BaseContractViolation): diff --git a/src/importspy/utilities/python_util.py b/src/importspy/utilities/python_util.py index eca7867..0197025 100644 --- a/src/importspy/utilities/python_util.py +++ b/src/importspy/utilities/python_util.py @@ -50,9 +50,10 @@ def extract_python_version(self) -> str: Example ------- >>> PythonUtil().extract_python_version() - '3.11.2' + '3.11' """ - return platform.python_version() + python_version = platform.python_version() + return ".".join(python_version.split(".")[:2]) def extract_python_implementation(self) -> str: """ From bb7d3f8fd4c6a50080c7aaf967b49dbdb646808b Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Sun, 18 May 2025 16:09:26 +0200 Subject: [PATCH 08/40] feat(models, validators): introduce RuntimeContractViolation for structured runtime errors - Added `RuntimeContractViolation` class to represent runtime-level contract violations with contextual error formatting. - Created `RuntimeBundle` dataclass to encapsulate runtime input for error label rendering. - Updated `RuntimeValidator` to raise structured error using `RuntimeContractViolation` when runtimes are missing. - Extended `FunctionContractViolation` to support module-level bundles for consistency. This enhancement enables type-safe and context-aware error reporting for runtime validation in ImportSpy. --- src/importspy/models.py | 21 ++++++++++++++++++--- src/importspy/validators.py | 19 +++++++++++++++---- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/importspy/models.py b/src/importspy/models.py index d825a66..84729ea 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -13,7 +13,8 @@ from typing import ( Optional, Union, - Any + Any, + List ) from abc import ( @@ -289,7 +290,7 @@ def from_contract_violation(cls, contract_violation: 'ContractViolation'): def render_message(self) -> str: return f"[{self.title}] {self.description} {self.solution}" - + class ContractViolation(ABC): @property @@ -357,7 +358,7 @@ def label(self) -> str: class FunctionContractViolation(BaseContractViolation): - def __init__(self, context:str, category:str, bundle:Union['ClassBundle']): + def __init__(self, context:str, category:str, bundle:Union['ClassBundle', 'ModuleBundle']): super().__init__(context, category) self.bundle = bundle @@ -365,6 +366,16 @@ def __init__(self, context:str, category:str, bundle:Union['ClassBundle']): def label(self) -> str: return Errors.FUNCTIONS_LABEL_TEMPLATE[self.context].format(**asdict(self.bundle)) +class RuntimeContractViolation(BaseContractViolation): + + def __init__(self, context:str, category:str, bundle:'RuntimeBundle'): + super().__init__(context, category) + self.bundle = bundle + + @property + def label(self) -> str: + return self.bundle.runtimes_1 + @dataclass class ClassBundle: @@ -387,5 +398,9 @@ class EnvironmentBundle: environment_variable_name: Optional[str] = None +class RuntimeBundle: + + runtimes_1: Optional[List[Runtime]] = None + diff --git a/src/importspy/validators.py b/src/importspy/validators.py index 06a236b..4eb9e7f 100644 --- a/src/importspy/validators.py +++ b/src/importspy/validators.py @@ -4,11 +4,17 @@ System, Environment, Python, - Module + Module, + Variable, + RuntimeContractViolation, + RuntimeBundle ) -from .constants import Constants - +from .constants import ( + Constants, + Contexts, + Errors +) class RuntimeValidator: @@ -21,7 +27,12 @@ def validate( return if not runtimes_2: - raise ValueError(Errors.ELEMENT_MISSING.format(runtimes_1)) + raise ValueError( + RuntimeContractViolation( + Contexts.RUNTIME_CONTEXT, + Errors.Category.MISSING, + RuntimeBundle(runtimes_1) + ).missing_error_handler()) runtime_2 = runtimes_2[0] From b1d4627ed987c9d2100a306ee7239a8b4c64dc6d Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Sun, 18 May 2025 19:22:36 +0200 Subject: [PATCH 09/40] feat(errors): add structured contract violations for system, python, and module layers - Introduced `SystemContractViolation`, `PythonContractViolation`, and `ModuleContractViolation` classes to support context-aware error rendering. - Extended `ModuleBundle` and added `SystemBundle`, `PythonBundle` to carry context-specific metadata. - Added `MODULE_LABEL_TEMPLATE` to support dynamic error label formatting for modules. - Updated `validators.py` to raise structured violations with accurate labels for missing systems, Python configurations, modules, and environments. This improves consistency, granularity, and future serializability of validation errors. --- src/importspy/constants.py | 7 ++++++- src/importspy/models.py | 42 ++++++++++++++++++++++++++++++++++++- src/importspy/validators.py | 38 ++++++++++++++++++++++++++++----- 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/src/importspy/constants.py b/src/importspy/constants.py index 6c04a9a..a8a7c58 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -132,10 +132,15 @@ class Category(str, Enum): } FUNCTIONS_LABEL_TEMPLATE = { - Contexts.MODULE_CONTEXT: 'The function "{function_name}" in module "{module_name}"', + Contexts.MODULE_CONTEXT: 'The function "{function_name}" in module "{filename}"', Contexts.CLASS_CONTEXT: 'The method "{method_name}" in class "{class_name}"' } + MODULE_LABEL_TEMPLATE = { + Contexts.RUNTIME_CONTEXT: 'The filename "{filename}"', + Contexts.ENVIRONMENT_CONTEXT: 'The version "{version}" of module "{filename}"' + } + ERROR_MESSAGE_TEMPLATES = { Category.MISSING: { TEMPLATE_KEY: "{label} is declared but missing.", diff --git a/src/importspy/models.py b/src/importspy/models.py index 84729ea..6eaa852 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -375,6 +375,37 @@ def __init__(self, context:str, category:str, bundle:'RuntimeBundle'): @property def label(self) -> str: return self.bundle.runtimes_1 + +class SystemContractViolation(BaseContractViolation): + + def __init__(self, context:str, category:str, bundle:'SystemBundle'): + super().__init__(context, category) + self.bundle = bundle + + @property + def label(self) -> str: + return self.bundle.systems_1 + +class PythonContractViolation(BaseContractViolation): + + def __init__(self, context:str, category:str, bundle:'PythonBundle'): + super().__init__(context, category) + self.bundle = bundle + + @property + def label(self) -> str: + return self.bundle.python_1 + + +class ModuleContractViolation(BaseContractViolation): + + def __init__(self, context:str, category:str, bundle:'ModuleBundle'): + super().__init__(context, category) + self.bundle = bundle + + @property + def label(self) -> str: + return Errors.MODULE_LABEL_TEMPLATE[self.context].format(**asdict(self.bundle)) @dataclass class ClassBundle: @@ -389,9 +420,10 @@ class ClassBundle: class ModuleBundle: variable_name: Optional[str] = None - module_name: Optional[str] = None + filename: Optional[str] = None argument_name: Optional[str] = None function_name: Optional[str] = None + version: Optional[str] = None @dataclass class EnvironmentBundle: @@ -402,5 +434,13 @@ class RuntimeBundle: runtimes_1: Optional[List[Runtime]] = None +class SystemBundle: + + systems_1: Optional[List[System]] = None + +class PythonBundle: + + python_1: Optional[Python] = None + diff --git a/src/importspy/validators.py b/src/importspy/validators.py index 4eb9e7f..169574e 100644 --- a/src/importspy/validators.py +++ b/src/importspy/validators.py @@ -7,7 +7,15 @@ Module, Variable, RuntimeContractViolation, - RuntimeBundle + RuntimeBundle, + SystemContractViolation, + SystemBundle, + EnvironmentBundle, + VariableContractViolation, + PythonContractViolation, + PythonBundle, + ModuleContractViolation, + ModuleBundle ) from .constants import ( @@ -57,7 +65,12 @@ def validate( return if not systems_2: - raise ValueError(Errors.ELEMENT_MISSING.format(systems_1)) + raise ValueError( + SystemContractViolation( + Contexts.RUNTIME_CONTEXT, + Errors.Category.MISSING, + SystemBundle(systems_1) + ).missing_error_handler()) system_2 = systems_2[0] @@ -77,7 +90,12 @@ def validate(self, return if not environment_2: - raise ValueError(Errors.ELEMENT_MISSING.format(environment_1)) + raise ValueError( + VariableContractViolation( + Contexts.ENVIRONMENT_CONTEXT, + Errors.Category.MISSING, + EnvironmentBundle(environment_1) + ).missing_error_handler()) variables_2 = environment_2.variables @@ -97,7 +115,12 @@ def validate( return if not pythons_2: - raise ValueError(Errors.ELEMENT_MISSING.format(pythons_1)) + raise ValueError( + PythonContractViolation( + Contexts.RUNTIME_CONTEXT, + Errors.Category.MISSING, + PythonBundle(python_1) + ).missing_error_handler()) python_2 = pythons_2[0] for python_1 in pythons_1: @@ -158,7 +181,12 @@ def validate( return if not module_2: - raise ValueError(Errors.ELEMENT_MISSING.format(modules_1)) + raise ValueError( + ModuleContractViolation( + Contexts.RUNTIME_CONTEXT, + Errors.Category.MISSING, + PythonBundle(python_1) + ).missing_error_handler()) for module_1 in modules_1: From 64333b96450e817685ee01ca218025009cdf0cc3 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Sun, 18 May 2025 23:39:15 +0200 Subject: [PATCH 10/40] refactor(errors): centralize contract violation handling and improve label rendering - Replaced per-category constructors with context-only `BaseContractViolation` base class. - Simplified construction of contract violation instances across system, runtime, python, module, variable, and function levels. - Updated `MODULE_LABEL_TEMPLATE` to refer to "module" instead of "filename" for clearer error messaging. - Refactored `VariableValidator` and `FunctionValidator` to receive structured violation context via `ContractViolation` subclasses. - Improved error messaging granularity and centralized mismatch/missing/invalid logic. This paves the way for fully contextualized error handling and better user-facing diagnostics. --- src/importspy/constants.py | 2 +- src/importspy/models.py | 40 ++++++-------- src/importspy/validators.py | 103 +++++++++++++++++++----------------- 3 files changed, 73 insertions(+), 72 deletions(-) diff --git a/src/importspy/constants.py b/src/importspy/constants.py index a8a7c58..0037f9b 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -137,7 +137,7 @@ class Category(str, Enum): } MODULE_LABEL_TEMPLATE = { - Contexts.RUNTIME_CONTEXT: 'The filename "{filename}"', + Contexts.RUNTIME_CONTEXT: 'The module "{filename}"', Contexts.ENVIRONMENT_CONTEXT: 'The version "{version}" of module "{filename}"' } diff --git a/src/importspy/models.py b/src/importspy/models.py index 6eaa852..b87dbe7 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -322,33 +322,28 @@ def invalid_error_handler(self) -> str: class BaseContractViolation(ContractViolation): - def __init__(self, context, category): + def __init__(self, context): self._context = context - self._category = category super().__init__() @property def context(self) -> str: return self._context - @property - def category(self) -> str: - return self._category - def missing_error_handler(self) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label).capitalize()}' + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label).capitalize()}' def mismatch_error_handler(self, expected:Any, actual:Any) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label, expected=expected, actual=actual).capitalize()}' + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label, expected=expected, actual=actual).capitalize()}' def invalid_error_handler(self, allowed:Any, found:Any) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label, expected=allowed, actual=found).capitalize()}' + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label, expected=allowed, actual=found).capitalize()}' class VariableContractViolation(BaseContractViolation): - def __init__(self, scope:str, context:str, category:str, bundle:Union['ModuleBundle' ,'ClassBundle', 'EnvironmentBundle']): - super().__init__(context, category) + def __init__(self, scope:str, context:str, bundle:Union['ModuleBundle' ,'ClassBundle', 'EnvironmentBundle']): + super().__init__(context) self.scope = scope self.bundle = bundle @@ -358,8 +353,8 @@ def label(self) -> str: class FunctionContractViolation(BaseContractViolation): - def __init__(self, context:str, category:str, bundle:Union['ClassBundle', 'ModuleBundle']): - super().__init__(context, category) + def __init__(self, context:str, bundle:Union['ClassBundle', 'ModuleBundle']): + super().__init__(context) self.bundle = bundle @property @@ -368,8 +363,8 @@ def label(self) -> str: class RuntimeContractViolation(BaseContractViolation): - def __init__(self, context:str, category:str, bundle:'RuntimeBundle'): - super().__init__(context, category) + def __init__(self, context:str, bundle:'RuntimeBundle'): + super().__init__(context) self.bundle = bundle @property @@ -378,8 +373,8 @@ def label(self) -> str: class SystemContractViolation(BaseContractViolation): - def __init__(self, context:str, category:str, bundle:'SystemBundle'): - super().__init__(context, category) + def __init__(self, context:str, bundle:'SystemBundle'): + super().__init__(context) self.bundle = bundle @property @@ -388,8 +383,8 @@ def label(self) -> str: class PythonContractViolation(BaseContractViolation): - def __init__(self, context:str, category:str, bundle:'PythonBundle'): - super().__init__(context, category) + def __init__(self, context:str, bundle:'PythonBundle'): + super().__init__(context) self.bundle = bundle @property @@ -399,8 +394,8 @@ def label(self) -> str: class ModuleContractViolation(BaseContractViolation): - def __init__(self, context:str, category:str, bundle:'ModuleBundle'): - super().__init__(context, category) + def __init__(self, context:str, bundle:'ModuleBundle'): + super().__init__(context) self.bundle = bundle @property @@ -420,10 +415,9 @@ class ClassBundle: class ModuleBundle: variable_name: Optional[str] = None - filename: Optional[str] = None + module_1: Optional[Module] = None argument_name: Optional[str] = None function_name: Optional[str] = None - version: Optional[str] = None @dataclass class EnvironmentBundle: diff --git a/src/importspy/validators.py b/src/importspy/validators.py index 169574e..7044008 100644 --- a/src/importspy/validators.py +++ b/src/importspy/validators.py @@ -6,6 +6,7 @@ Python, Module, Variable, + Function, RuntimeContractViolation, RuntimeBundle, SystemContractViolation, @@ -15,7 +16,10 @@ PythonContractViolation, PythonBundle, ModuleContractViolation, - ModuleBundle + ModuleBundle, + BaseContractViolation, + FunctionContractViolation, + ClassBundle ) from .constants import ( @@ -24,6 +28,8 @@ Errors ) +from .log_manager import LogManager + class RuntimeValidator: def validate( @@ -38,7 +44,6 @@ def validate( raise ValueError( RuntimeContractViolation( Contexts.RUNTIME_CONTEXT, - Errors.Category.MISSING, RuntimeBundle(runtimes_1) ).missing_error_handler()) @@ -68,7 +73,6 @@ def validate( raise ValueError( SystemContractViolation( Contexts.RUNTIME_CONTEXT, - Errors.Category.MISSING, SystemBundle(systems_1) ).missing_error_handler()) @@ -93,7 +97,6 @@ def validate(self, raise ValueError( VariableContractViolation( Contexts.ENVIRONMENT_CONTEXT, - Errors.Category.MISSING, EnvironmentBundle(environment_1) ).missing_error_handler()) @@ -118,7 +121,6 @@ def validate( raise ValueError( PythonContractViolation( Contexts.RUNTIME_CONTEXT, - Errors.Category.MISSING, PythonBundle(python_1) ).missing_error_handler()) @@ -172,6 +174,10 @@ def _is_python_match( class ModuleValidator: + def __init__(self): + self.variable_validator:VariableValidator = VariableValidator() + self.function_validator:FunctionValidator = FunctionValidator() + def validate( self, modules_1: List[Module], @@ -184,25 +190,42 @@ def validate( raise ValueError( ModuleContractViolation( Contexts.RUNTIME_CONTEXT, - Errors.Category.MISSING, - PythonBundle(python_1) + ModuleBundle(module_1) ).missing_error_handler()) for module_1 in modules_1: if module_1.filename and module_1.filename != module_2.filename: - raise ValueError(Errors.FILENAME_MISMATCH.format(module_1.filename, module_2.filename)) + raise ValueError( + ModuleContractViolation( + Contexts.RUNTIME_CONTEXT, + ModuleBundle(module_1) + ).mismatch_error_handler(module_1.filename, module_2.filename)) if module_1.version and module_1.version != module_2.version: - raise ValueError(Errors.VERSION_MISMATCH.format(module_1.version, module_2.version)) + raise ValueError( + ModuleContractViolation( + Contexts.RUNTIME_CONTEXT, + ModuleBundle(module_1) + ).mismatch_error_handler(module_1.version, module_2.version)) - self._variable_validator.validate( + self.variable_validator.validate( module_1.variables, - module_2.variables) + module_2.variables, + VariableContractViolation( + Errors.SCOPE_VARIABLE, + Contexts.MODULE_CONTEXT, + ModuleBundle(module_1) + ) + ) - self._function_validator.validate( + self.function_validator.validate( module_1.functions, - module_2.functions + module_2.functions, + FunctionContractViolation( + Contexts.MODULE_CONTEXT, + ModuleBundle(module_1) + ) ) if module_1.classes: @@ -211,10 +234,11 @@ def validate( if not class_2: raise ValueError(Errors.CLASS_MISSING.format(class_1.name)) - self._attribute_validator.validate( + self.variable_validator.validate( class_1.attributes, class_2.attributes, - class_1.name + VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.CLASS_CONTEXT, ClassBundle()) + ### Warning: can't known attribute type at this time ) self._function_validator.validate( @@ -241,22 +265,9 @@ def validate( self, variables_1: List[Variable], variables_2: List[Variable], + contract_violation: BaseContractViolation ): - """ - Validate two sets of variables for presence, value match, and type annotations. - - Parameters - ---------- - variables_1 : List[Variable] - The list of expected variables (from the contract). - variables_2 : List[Variable] - The list of actual variables (from the module/system). - - Raises - ------ - ValueError - If a variable is missing, has a mismatched value, or fails type annotation validation. - """ + self.logger.debug(f"Type of variables_1: {type(variables_1)}") self.logger.debug( Constants.LOG_MESSAGE_TEMPLATE.format( @@ -284,7 +295,7 @@ def validate( details="No actual Variables found for validation" ) ) - raise ValueError(self.context.format_missing_error()) + raise ValueError(contract_violation.missing_error_handler(variables_1)) for vars_1 in variables_1: self.logger.debug( @@ -295,33 +306,31 @@ def validate( ) ) if vars_1.name not in {var.name for var in variables_2}: - raise ValueError(self.context.format_missing_error()) + raise ValueError(contract_violation.missing_error_handler(vars_1.name)) for vars_1 in variables_1: vars_2 = next((var for var in variables_2 if var.name == vars_1.name), None) if not vars_2: - raise ValueError(self.context.format_missing_error()) + raise ValueError(contract_violation.missing_error_handler(vars_1.name)) if vars_1.annotation and vars_1.annotation != vars_2.annotation: - raise ValueError( - self.context.format_mismatch_error(vars_1.annotation, vars_2.annotation) - ) + raise ValueError(contract_violation.mismatch_error_handler(vars_1.annotation, vars_2.annotation)) if vars_1.value != vars_2.value: - raise ValueError( - self.context.format_mismatch_error(vars_1.value, vars_2.value) - ) + raise ValueError(contract_violation.mismatch_error_handler(vars_1.value, vars_2.value)) class FunctionValidator: def __init__(self): + self.argument_validator:VariableValidator = VariableValidator() self.logger = LogManager().get_logger(self.__class__.__name__) def validate( self, functions_1: List[Function], functions_2: List[Function], + contract_violation: BaseContractViolation ): self.logger.debug( @@ -340,7 +349,7 @@ def validate( details="No functions to validate" ) ) - return None + return if not functions_2: self.logger.debug( @@ -350,7 +359,7 @@ def validate( details="No actual functions found" ) ) - raise ValueError(Errors.ELEMENT_MISSING.format(functions_1)) + raise ValueError(contract_violation.missing_error_handler(functions_1)) for function_1 in functions_1: self.logger.debug( @@ -368,20 +377,18 @@ def validate( details=f"function_1: {function_1}; functions_2: {functions_2}" ) ) - raise ValueError( - Errors.ELEMENT_MISMATCH.format(context_name, function_1.name) - ) + raise ValueError(contract_violation.missing_error_handler(function_1)) for function_1 in functions_1: function_2 = next((f for f in functions_2 if f.name == function_1.name), None) if not function_2: - raise ValueError(Errors.ELEMENT_MISSING.format(function_1)) + raise ValueError(contract_violation.missing_error_handler/function_1) - self._argument_validator.validate( + self.argument_validator.validate( function_1.arguments, function_2.arguments, - function_1.name, - classname + VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.MODULE_CONTEXT) + ) if function_1.return_annotation and function_1.return_annotation != function_2.return_annotation: From 6494b92248d8c894665cf668d72d2825f64fcc73 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Tue, 20 May 2025 14:45:42 +0200 Subject: [PATCH 11/40] refactor(class-validator): isolate class vs instance attributes for scoped validation - Added `get_class_attributes()` and `get_instance_attributes()` to `Class` model to enable scoped attribute validation. - Updated `ModuleValidator` to use `get_class_attributes()` for clearer intent and improved context. - Injected `attribute_type` and `class_name` into `VariableContractViolation` for better error label rendering. - Removed unused `attribute_name` field from `ClassBundle`. This refactor improves precision of class attribute validation and lays groundwork for separate handling of instance attributes. --- src/importspy/models.py | 10 +++++++++- src/importspy/validators.py | 7 +++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/importspy/models.py b/src/importspy/models.py index b87dbe7..95f6e82 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -38,6 +38,9 @@ Contexts, Errors ) + +from .config import Config + import logging logger = logging.getLogger("/".join(__file__.split('/')[-2:])) @@ -182,6 +185,12 @@ def from_class_info(cls, extracted_classes: list[ClassInfo]): methods=Function.from_functions_info(methods), superclasses=superclasses ) for name, attributes, methods, superclasses in extracted_classes] + + def get_class_attributes(self) -> List[Attribute]: + return [attr for attr in self.attributes if attr.type == Config.CLASS_TYPE] + + def get_instance_attributes(self) -> List[Attribute]: + return [attr for attr in self.attributes if attr.type == Config.INSTANCE_TYPE] class Module(BaseModel): @@ -407,7 +416,6 @@ class ClassBundle: class_name: Optional[str] = None attribute_type: Optional[str] = None - attribute_name: Optional[str] = None argument_name: Optional[str] = None method_name: Optional[str] = None diff --git a/src/importspy/validators.py b/src/importspy/validators.py index 7044008..4cd7972 100644 --- a/src/importspy/validators.py +++ b/src/importspy/validators.py @@ -235,10 +235,9 @@ def validate( raise ValueError(Errors.CLASS_MISSING.format(class_1.name)) self.variable_validator.validate( - class_1.attributes, - class_2.attributes, - VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.CLASS_CONTEXT, ClassBundle()) - ### Warning: can't known attribute type at this time + class_1.get_class_attributes(), + class_2.get_class_attributes(), + VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.CLASS_CONTEXT, ClassBundle(attribute_type="class", class_name=class_1.name)) ) self._function_validator.validate( From 1ec8612346c40dc269ea007143e82d12bf5f75ce Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Tue, 27 May 2025 19:56:15 +0200 Subject: [PATCH 12/40] refactor(models/validators): extract contract violation logic to dedicated module - Removed `ContractViolation` and related subclasses from `models.py` - Introduced `violation_systems.py` to isolate and encapsulate violation logic - Refactored `ModuleValidator` to delegate class validation to new `ClassValidator` - Added `ClassValidator` for structured validation of class attributes and methods - Improved modularity and readability of validation logic across layers This paves the way for scalable handling of violations across contexts while keeping `models.py` focused on data structure definitions. --- src/importspy/models.py | 151 -------------------------- src/importspy/validators.py | 82 +++++++++++---- src/importspy/violation_systems.py | 163 +++++++++++++++++++++++++++++ 3 files changed, 224 insertions(+), 172 deletions(-) create mode 100644 src/importspy/violation_systems.py diff --git a/src/importspy/models.py b/src/importspy/models.py index 95f6e82..f7d10b2 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -17,13 +17,6 @@ List ) -from abc import ( - ABC, - abstractmethod -) - -from dataclasses import dataclass, asdict - from types import ModuleType from .utilities.module_util import ( @@ -299,150 +292,6 @@ def from_contract_violation(cls, contract_violation: 'ContractViolation'): def render_message(self) -> str: return f"[{self.title}] {self.description} {self.solution}" - -class ContractViolation(ABC): - - @property - @abstractmethod - def context(self) -> str: - pass - - @property - @abstractmethod - def category(self) -> str: - pass - - @property - @abstractmethod - def label(self) -> str: - pass - - @abstractmethod - def missing_error_handler(self) -> str: - pass - - @abstractmethod - def mismatch_error_handler(self) -> str: - pass - - @abstractmethod - def invalid_error_handler(self) -> str: - pass - -class BaseContractViolation(ContractViolation): - - def __init__(self, context): - - self._context = context - super().__init__() - - @property - def context(self) -> str: - return self._context - - def missing_error_handler(self) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label).capitalize()}' - - def mismatch_error_handler(self, expected:Any, actual:Any) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label, expected=expected, actual=actual).capitalize()}' - - def invalid_error_handler(self, allowed:Any, found:Any) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label, expected=allowed, actual=found).capitalize()}' - -class VariableContractViolation(BaseContractViolation): - - def __init__(self, scope:str, context:str, bundle:Union['ModuleBundle' ,'ClassBundle', 'EnvironmentBundle']): - super().__init__(context) - self.scope = scope - self.bundle = bundle - - @property - def label(self) -> str: - return Errors.VARIABLES_LABEL_TEMPLATE[self.scope][self.context].format(**asdict(self.bundle)) - -class FunctionContractViolation(BaseContractViolation): - - def __init__(self, context:str, bundle:Union['ClassBundle', 'ModuleBundle']): - super().__init__(context) - self.bundle = bundle - - @property - def label(self) -> str: - return Errors.FUNCTIONS_LABEL_TEMPLATE[self.context].format(**asdict(self.bundle)) - -class RuntimeContractViolation(BaseContractViolation): - - def __init__(self, context:str, bundle:'RuntimeBundle'): - super().__init__(context) - self.bundle = bundle - - @property - def label(self) -> str: - return self.bundle.runtimes_1 - -class SystemContractViolation(BaseContractViolation): - - def __init__(self, context:str, bundle:'SystemBundle'): - super().__init__(context) - self.bundle = bundle - - @property - def label(self) -> str: - return self.bundle.systems_1 - -class PythonContractViolation(BaseContractViolation): - - def __init__(self, context:str, bundle:'PythonBundle'): - super().__init__(context) - self.bundle = bundle - - @property - def label(self) -> str: - return self.bundle.python_1 - - -class ModuleContractViolation(BaseContractViolation): - - def __init__(self, context:str, bundle:'ModuleBundle'): - super().__init__(context) - self.bundle = bundle - - @property - def label(self) -> str: - return Errors.MODULE_LABEL_TEMPLATE[self.context].format(**asdict(self.bundle)) - -@dataclass -class ClassBundle: - - class_name: Optional[str] = None - attribute_type: Optional[str] = None - argument_name: Optional[str] = None - method_name: Optional[str] = None - -@dataclass -class ModuleBundle: - - variable_name: Optional[str] = None - module_1: Optional[Module] = None - argument_name: Optional[str] = None - function_name: Optional[str] = None - -@dataclass -class EnvironmentBundle: - - environment_variable_name: Optional[str] = None - -class RuntimeBundle: - - runtimes_1: Optional[List[Runtime]] = None - -class SystemBundle: - - systems_1: Optional[List[System]] = None - -class PythonBundle: - - python_1: Optional[Python] = None diff --git a/src/importspy/validators.py b/src/importspy/validators.py index 4cd7972..a635c7d 100644 --- a/src/importspy/validators.py +++ b/src/importspy/validators.py @@ -7,6 +7,10 @@ Module, Variable, Function, + Class +) + +from .violation_systems import ( RuntimeContractViolation, RuntimeBundle, SystemContractViolation, @@ -177,6 +181,7 @@ class ModuleValidator: def __init__(self): self.variable_validator:VariableValidator = VariableValidator() self.function_validator:FunctionValidator = FunctionValidator() + self.class_validator:ClassValidator = ClassValidator() def validate( self, @@ -228,31 +233,66 @@ def validate( ) ) - if module_1.classes: - for class_1 in module_1.classes: - class_2 = next((cls for cls in module_2.classes if cls.name == class_1.name), None) - if not class_2: - raise ValueError(Errors.CLASS_MISSING.format(class_1.name)) + self.class_validator.validate( + module_1.classes, + module_2.classes + ) + - self.variable_validator.validate( - class_1.get_class_attributes(), - class_2.get_class_attributes(), - VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.CLASS_CONTEXT, ClassBundle(attribute_type="class", class_name=class_1.name)) - ) +class ClassValidator: - self._function_validator.validate( - class_1.methods, - class_2.methods, - classname=class_1.name - ) + def __init__(self): + + self.variable_validator:VariableValidator = VariableValidator() + self.function_validator:FunctionValidator = FunctionValidator() + + def validate( + self, + classes_1: List[Class], + classes_2: List[Class] + ): + if classes_1: + for class_1 in classes_1: + class_2 = next((cls for cls in classes_2 if cls.name == class_1.name), None) + if not class_2: + raise ValueError(Errors.CLASS_MISSING.format(class_1.name)) + + self.variable_validator.validate( + class_1.get_class_attributes(), + class_2.get_class_attributes(), + VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.CLASS_CONTEXT, ClassBundle(attribute_type="class", class_name=class_1.name)) + ) - CommonValidator().list_validate( - class_1.superclasses, - class_2.superclasses, - Errors.CLASS_SUPERCLASS_MISSING, - class_2.name + self.variable_validator.validate( + class_1.get_instance_attributes(), + class_2.get_instance_attributes(), + VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.CLASS_CONTEXT, ClassBundle(attribute_type="instance", class_name=class_1.name)) + ) + + self.function_validator.validate( + class_1.methods, + class_2.methods, + FunctionContractViolation( + Contexts.CLASS_CONTEXT, + ClassBundle( + class_name=class_1.name + ) ) - return + ) + + self._function_validator.validate( + class_1.methods, + class_2.methods, + classname=class_1.name + ) + + CommonValidator().list_validate( + class_1.superclasses, + class_2.superclasses, + Errors.CLASS_SUPERCLASS_MISSING, + class_2.name + ) + return class VariableValidator: diff --git a/src/importspy/violation_systems.py b/src/importspy/violation_systems.py new file mode 100644 index 0000000..5b79d94 --- /dev/null +++ b/src/importspy/violation_systems.py @@ -0,0 +1,163 @@ +from abc import ( + ABC, + abstractmethod +) + +from dataclasses import dataclass, asdict + +from typing import ( + Optional, + Union, + Any +) + +from .constants import Errors +from .models import ( + System, + Runtime, + Module, + Python +) + +class ContractViolation(ABC): + + @property + @abstractmethod + def context(self) -> str: + pass + + @property + @abstractmethod + def category(self) -> str: + pass + + @property + @abstractmethod + def label(self) -> str: + pass + + @abstractmethod + def missing_error_handler(self) -> str: + pass + + @abstractmethod + def mismatch_error_handler(self) -> str: + pass + + @abstractmethod + def invalid_error_handler(self) -> str: + pass + +class BaseContractViolation(ContractViolation): + + def __init__(self, context): + + self._context = context + super().__init__() + + @property + def context(self) -> str: + return self._context + + def missing_error_handler(self) -> str: + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label).capitalize()}' + + def mismatch_error_handler(self, expected:Any, actual:Any) -> str: + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label, expected=expected, actual=actual).capitalize()}' + + def invalid_error_handler(self, allowed:Any, found:Any) -> str: + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label, expected=allowed, actual=found).capitalize()}' + +class VariableContractViolation(BaseContractViolation): + + def __init__(self, scope:str, context:str, bundle:Union['ModuleBundle' ,'ClassBundle', 'EnvironmentBundle']): + super().__init__(context) + self.scope = scope + self.bundle = bundle + + @property + def label(self) -> str: + return Errors.VARIABLES_LABEL_TEMPLATE[self.scope][self.context].format(**asdict(self.bundle)) + +class FunctionContractViolation(BaseContractViolation): + + def __init__(self, context:str, bundle:Union['ClassBundle', 'ModuleBundle']): + super().__init__(context) + self.bundle = bundle + + @property + def label(self) -> str: + return Errors.FUNCTIONS_LABEL_TEMPLATE[self.context].format(**asdict(self.bundle)) + +class RuntimeContractViolation(BaseContractViolation): + + def __init__(self, context:str, bundle:'RuntimeBundle'): + super().__init__(context) + self.bundle = bundle + + @property + def label(self) -> str: + return self.bundle.runtimes_1 + +class SystemContractViolation(BaseContractViolation): + + def __init__(self, context:str, bundle:'SystemBundle'): + super().__init__(context) + self.bundle = bundle + + @property + def label(self) -> str: + return self.bundle.systems_1 + +class PythonContractViolation(BaseContractViolation): + + def __init__(self, context:str, bundle:'PythonBundle'): + super().__init__(context) + self.bundle = bundle + + @property + def label(self) -> str: + return self.bundle.python_1 + + +class ModuleContractViolation(BaseContractViolation): + + def __init__(self, context:str, bundle:'ModuleBundle'): + super().__init__(context) + self.bundle = bundle + + @property + def label(self) -> str: + return Errors.MODULE_LABEL_TEMPLATE[self.context].format(**asdict(self.bundle)) + +@dataclass +class ClassBundle: + + class_name: Optional[str] = None + attribute_type: Optional[str] = None + method_name: Optional[str] = None + +@dataclass +class ModuleBundle: + + variable_name: Optional[str] = None + module_1: Optional[Module] = None + argument_name: Optional[str] = None + function_name: Optional[str] = None + +@dataclass +class EnvironmentBundle: + + environment_variable_name: Optional[str] = None + +class RuntimeBundle: + + runtimes_1: Optional[list[Runtime]] = None + +class SystemBundle: + + systems_1: Optional[list[System]] = None + +class PythonBundle: + + python_1: Optional[Python] = None \ No newline at end of file From 96d48fd651a9212e226f85df7c076a09dab3a647 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Wed, 28 May 2025 20:56:03 +0200 Subject: [PATCH 13/40] refactor(validation): centralize violation payload with shared Bundle - Introduced `Bundle` dataclass to unify and track dynamic validation state - Replaced context-specific bundles (e.g. PythonBundle, ModuleBundle) with `Bundle` across validators and contract violations - Extended `Errors` with dynamic payload key constants (e.g. KEY_MODULE_NAME, KEY_FUNCTION_NAME) - Adjusted `Spy._validate_module()` and all validators to propagate and populate a single `Bundle` instance - Simplified label rendering by removing `asdict()` calls and using `bundle.state` This update reduces duplication, improves consistency across validations, and lays the groundwork for customizable error payload introspection. --- src/importspy/constants.py | 36 ++++++++++++++++ src/importspy/s.py | 11 +++-- src/importspy/validators.py | 58 ++++++++++++++++--------- src/importspy/violation_systems.py | 68 ++++++++---------------------- 4 files changed, 100 insertions(+), 73 deletions(-) diff --git a/src/importspy/constants.py b/src/importspy/constants.py index 0037f9b..82f0ca0 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -131,6 +131,42 @@ class Category(str, Enum): } } + KEY_RUNTIMES_1 = "runtimes_1" + KEY_SYSTEMS_1 = "runtimes_1" + KEY_PYTHON_1 = "python_1" + KEY_ENVIRONMENT_1 = "environment_1" + KEY_ENVIRONMENT_VARIABLE = "environment_variable_name" + KEY_MODULES_1 = "modules_1" + KEY_VARIABLE_NAME = "variable_name" + KEY_ARGUMENT_NAME = "argument_name" + KEY_FUNCTION_NAME = "function_name" + KEY_METHOD_NAME = "method_name" + KEY_MODULE_NAME = "module_name" + KEY_ATTRIBUTE_TYPE = "attribute_type" + KEY_ATTRIBUTE_NAME = "attribute_name" + KEY_CLASS_NAME = "class_name" + KEY_MODULE_VERSION = "version" + KEY_FILE_NAME = "filename" + + VARIABLES_DINAMIC_PAYLOAD = { + + SCOPE_VARIABLE: { + Contexts.ENVIRONMENT_CONTEXT: KEY_ENVIRONMENT_VARIABLE, + Contexts.MODULE_CONTEXT: KEY_VARIABLE_NAME, + Contexts.CLASS_CONTEXT: KEY_ATTRIBUTE_NAME + }, + SCOPE_ARGUMENT: { + Contexts.MODULE_CONTEXT: KEY_ARGUMENT_NAME, + Contexts.CLASS_CONTEXT: KEY_ARGUMENT_NAME + } + + } + + FUNCTIONS_DINAMIC_PAYLOAD = { + Contexts.MODULE_CONTEXT: KEY_FUNCTION_NAME, + Contexts.CLASS_CONTEXT: KEY_METHOD_NAME + } + FUNCTIONS_LABEL_TEMPLATE = { Contexts.MODULE_CONTEXT: 'The function "{function_name}" in module "{filename}"', Contexts.CLASS_CONTEXT: 'The method "{method_name}" in class "{class_name}"' diff --git a/src/importspy/s.py b/src/importspy/s.py index 2da4b37..1dee4a7 100644 --- a/src/importspy/s.py +++ b/src/importspy/s.py @@ -42,6 +42,8 @@ ) import logging +from .violation_systems import Bundle + class Spy: """ @@ -150,14 +152,15 @@ def _configure_logging(self, log_level: Optional[int] = None): def _validate_module(self, spymodel: SpyModel, info_module: ModuleType) -> ModuleType: self.logger.debug(f"info_module: {info_module}") if spymodel: - module_validator:ModuleValidator = ModuleValidator() + bundle = Bundle() + module_validator:ModuleValidator = ModuleValidator(bundle) self.logger.debug(f"Import contract detected: {spymodel}") spy_module = SpyModel.from_module(info_module) self.logger.debug(f"Extracted module structure: {spy_module}") module_validator.validate([spymodel],spy_module.deployments[0].systems[0].pythons[0].modules[0]) - runtime:Runtime = RuntimeValidator().validate(spymodel.deployments, spy_module.deployments) - pythons:List[Python] = SystemValidator().validate(runtime.systems, spy_module.deployments[0].systems) - modules: List[Module] = PythonValidator().validate(pythons, spy_module.deployments[0].systems[0].pythons) + runtime:Runtime = RuntimeValidator(bundle).validate(spymodel.deployments, spy_module.deployments) + pythons:List[Python] = SystemValidator(bundle).validate(runtime.systems, spy_module.deployments[0].systems) + modules: List[Module] = PythonValidator(bundle).validate(pythons, spy_module.deployments[0].systems[0].pythons) module_validator.validate(modules, spy_module.deployments[0].systems[0].pythons[0].modules[0]) return ModuleUtil().load_module(info_module) diff --git a/src/importspy/validators.py b/src/importspy/validators.py index a635c7d..d25bcd8 100644 --- a/src/importspy/validators.py +++ b/src/importspy/validators.py @@ -12,18 +12,13 @@ from .violation_systems import ( RuntimeContractViolation, - RuntimeBundle, SystemContractViolation, - SystemBundle, - EnvironmentBundle, VariableContractViolation, PythonContractViolation, - PythonBundle, ModuleContractViolation, - ModuleBundle, BaseContractViolation, FunctionContractViolation, - ClassBundle + Bundle ) from .constants import ( @@ -36,6 +31,9 @@ class RuntimeValidator: + def __init__(self, bundle:Bundle): + self.bundle = bundle + def validate( self, runtimes_1: List[Runtime], @@ -43,12 +41,14 @@ def validate( ): if not runtimes_1: return + + self.bundle.state[Errors.KEY_RUNTIMES_1] = runtimes_1 if not runtimes_2: raise ValueError( RuntimeContractViolation( Contexts.RUNTIME_CONTEXT, - RuntimeBundle(runtimes_1) + self.bundle ).missing_error_handler()) runtime_2 = runtimes_2[0] @@ -60,8 +60,9 @@ def validate( class SystemValidator: - def __init__(self): - + def __init__(self, bundle:Bundle): + + self.bundle = bundle self._environment_validator = SystemValidator.EnvironmentValidator() def validate( @@ -72,12 +73,14 @@ def validate( if not systems_1: return + + self.bundle.state[Errors.KEY_SYSTEMS_1] = systems_1 if not systems_2: raise ValueError( SystemContractViolation( Contexts.RUNTIME_CONTEXT, - SystemBundle(systems_1) + self.bundle ).missing_error_handler()) system_2 = systems_2[0] @@ -89,19 +92,25 @@ def validate( return system_1.pythons class EnvironmentValidator: + + def __init__(self, bundle:Bundle): + self.bundle = bundle def validate(self, environment_1: Environment, - environment_2: Environment): + environment_2: Environment + ): if not environment_1: return + + self.bundle.state[Errors.KEY_ENVIRONMENT_VARIABLE] = environment_1 if not environment_2: raise ValueError( VariableContractViolation( Contexts.ENVIRONMENT_CONTEXT, - EnvironmentBundle(environment_1) + self.bundle ).missing_error_handler()) variables_2 = environment_2.variables @@ -113,6 +122,9 @@ def validate(self, class PythonValidator: + def __init__(self, bundle:Bundle): + self.bundle = bundle + def validate( self, pythons_1: List[Python], @@ -120,12 +132,14 @@ def validate( ): if not pythons_1: return + + self.bundle.state[Errors.KEY_PYTHON_1] = python_1 if not pythons_2: raise ValueError( PythonContractViolation( Contexts.RUNTIME_CONTEXT, - PythonBundle(python_1) + self.bundle ).missing_error_handler()) python_2 = pythons_2[0] @@ -178,7 +192,8 @@ def _is_python_match( class ModuleValidator: - def __init__(self): + def __init__(self, bundle:Bundle): + self.bundle = bundle self.variable_validator:VariableValidator = VariableValidator() self.function_validator:FunctionValidator = FunctionValidator() self.class_validator:ClassValidator = ClassValidator() @@ -190,28 +205,33 @@ def validate( ): if not modules_1: return + + self.bundle.state[Errors.KEY_MODULES_1] = modules_1 if not module_2: raise ValueError( ModuleContractViolation( Contexts.RUNTIME_CONTEXT, - ModuleBundle(module_1) + self.bundle ).missing_error_handler()) for module_1 in modules_1: + self.bundle.state[Errors.KEY_MODULE_NAME] = module_1.filename + self.bundle.state[Errors.KEY_MODULE_VERSION] = module_1.version + if module_1.filename and module_1.filename != module_2.filename: raise ValueError( ModuleContractViolation( Contexts.RUNTIME_CONTEXT, - ModuleBundle(module_1) + self.bundle ).mismatch_error_handler(module_1.filename, module_2.filename)) if module_1.version and module_1.version != module_2.version: raise ValueError( ModuleContractViolation( Contexts.RUNTIME_CONTEXT, - ModuleBundle(module_1) + self.bundle ).mismatch_error_handler(module_1.version, module_2.version)) self.variable_validator.validate( @@ -220,7 +240,7 @@ def validate( VariableContractViolation( Errors.SCOPE_VARIABLE, Contexts.MODULE_CONTEXT, - ModuleBundle(module_1) + self.bundle ) ) @@ -229,7 +249,7 @@ def validate( module_2.functions, FunctionContractViolation( Contexts.MODULE_CONTEXT, - ModuleBundle(module_1) + self.bundle ) ) diff --git a/src/importspy/violation_systems.py b/src/importspy/violation_systems.py index 5b79d94..a164e75 100644 --- a/src/importspy/violation_systems.py +++ b/src/importspy/violation_systems.py @@ -3,21 +3,17 @@ abstractmethod ) -from dataclasses import dataclass, asdict +from dataclasses import ( + dataclass, + field +) from typing import ( Optional, - Union, Any ) from .constants import Errors -from .models import ( - System, - Runtime, - Module, - Python -) class ContractViolation(ABC): @@ -70,94 +66,66 @@ def invalid_error_handler(self, allowed:Any, found:Any) -> str: class VariableContractViolation(BaseContractViolation): - def __init__(self, scope:str, context:str, bundle:Union['ModuleBundle' ,'ClassBundle', 'EnvironmentBundle']): + def __init__(self, scope:str, context:str, bundle:'Bundle'): super().__init__(context) self.scope = scope self.bundle = bundle @property def label(self) -> str: - return Errors.VARIABLES_LABEL_TEMPLATE[self.scope][self.context].format(**asdict(self.bundle)) + return Errors.VARIABLES_LABEL_TEMPLATE[self.scope][self.context].format(**self.bundle.state) class FunctionContractViolation(BaseContractViolation): - def __init__(self, context:str, bundle:Union['ClassBundle', 'ModuleBundle']): + def __init__(self, context:str, bundle:'Bundle'): super().__init__(context) self.bundle = bundle @property def label(self) -> str: - return Errors.FUNCTIONS_LABEL_TEMPLATE[self.context].format(**asdict(self.bundle)) + return Errors.FUNCTIONS_LABEL_TEMPLATE[self.context].format(**self.bundle.state) class RuntimeContractViolation(BaseContractViolation): - def __init__(self, context:str, bundle:'RuntimeBundle'): + def __init__(self, context:str, bundle:'Bundle'): super().__init__(context) self.bundle = bundle @property def label(self) -> str: - return self.bundle.runtimes_1 + return self.bundle.state[Errors.KEY_RUNTIMES_1] class SystemContractViolation(BaseContractViolation): - def __init__(self, context:str, bundle:'SystemBundle'): + def __init__(self, context:str, bundle:'Bundle'): super().__init__(context) self.bundle = bundle @property def label(self) -> str: - return self.bundle.systems_1 + return self.bundle.state[Errors.KEY_SYSTEMS_1] class PythonContractViolation(BaseContractViolation): - def __init__(self, context:str, bundle:'PythonBundle'): + def __init__(self, context:str, bundle:'Bundle'): super().__init__(context) self.bundle = bundle @property def label(self) -> str: - return self.bundle.python_1 - + return self.bundle.state[Errors.KEY_PYTHON_1] class ModuleContractViolation(BaseContractViolation): - def __init__(self, context:str, bundle:'ModuleBundle'): + def __init__(self, context:str, bundle:'Bundle'): super().__init__(context) self.bundle = bundle @property def label(self) -> str: - return Errors.MODULE_LABEL_TEMPLATE[self.context].format(**asdict(self.bundle)) - -@dataclass -class ClassBundle: - - class_name: Optional[str] = None - attribute_type: Optional[str] = None - method_name: Optional[str] = None + return Errors.MODULE_LABEL_TEMPLATE[self.context].format(**self.bundle.state) @dataclass -class ModuleBundle: - - variable_name: Optional[str] = None - module_1: Optional[Module] = None - argument_name: Optional[str] = None - function_name: Optional[str] = None - -@dataclass -class EnvironmentBundle: - - environment_variable_name: Optional[str] = None - -class RuntimeBundle: - - runtimes_1: Optional[list[Runtime]] = None - -class SystemBundle: - - systems_1: Optional[list[System]] = None - -class PythonBundle: +class Bundle: - python_1: Optional[Python] = None \ No newline at end of file + state: Optional[dict[str, Any]] = field(default_factory=dict) \ No newline at end of file From 06ccd897ea1868ef375495716ad220e863b5174c Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Thu, 29 May 2025 14:38:48 +0200 Subject: [PATCH 14/40] refactor(bundle): enable dict-like access to Bundle for cleaner usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replaced explicit .state access with magic methods (__getitem__, __setitem__) - Renamed KEY_ENVIRONMENT_VARIABLE → KEY_ENVIRONMENT_VARIABLE_NAME for consistency - Registered variable names in bundle dynamically during validation - Updated all contract violations to use dict-style access on Bundle --- src/importspy/constants.py | 5 +-- src/importspy/validators.py | 47 ++++++++++++++-------------- src/importspy/violation_systems.py | 49 ++++++++++++++++++------------ 3 files changed, 57 insertions(+), 44 deletions(-) diff --git a/src/importspy/constants.py b/src/importspy/constants.py index 82f0ca0..04b442f 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -135,8 +135,9 @@ class Category(str, Enum): KEY_SYSTEMS_1 = "runtimes_1" KEY_PYTHON_1 = "python_1" KEY_ENVIRONMENT_1 = "environment_1" - KEY_ENVIRONMENT_VARIABLE = "environment_variable_name" + KEY_ENVIRONMENT_VARIABLE_NAME = "environment_variable_name" KEY_MODULES_1 = "modules_1" + KEY_VARIABLES_1 = "variables_1" KEY_VARIABLE_NAME = "variable_name" KEY_ARGUMENT_NAME = "argument_name" KEY_FUNCTION_NAME = "function_name" @@ -151,7 +152,7 @@ class Category(str, Enum): VARIABLES_DINAMIC_PAYLOAD = { SCOPE_VARIABLE: { - Contexts.ENVIRONMENT_CONTEXT: KEY_ENVIRONMENT_VARIABLE, + Contexts.ENVIRONMENT_CONTEXT: KEY_ENVIRONMENT_VARIABLE_NAME, Contexts.MODULE_CONTEXT: KEY_VARIABLE_NAME, Contexts.CLASS_CONTEXT: KEY_ATTRIBUTE_NAME }, diff --git a/src/importspy/validators.py b/src/importspy/validators.py index d25bcd8..8d62544 100644 --- a/src/importspy/validators.py +++ b/src/importspy/validators.py @@ -42,7 +42,7 @@ def validate( if not runtimes_1: return - self.bundle.state[Errors.KEY_RUNTIMES_1] = runtimes_1 + self.bundle[Errors.KEY_RUNTIMES_1] = runtimes_1 if not runtimes_2: raise ValueError( @@ -74,7 +74,7 @@ def validate( if not systems_1: return - self.bundle.state[Errors.KEY_SYSTEMS_1] = systems_1 + self.bundle[Errors.KEY_SYSTEMS_1] = systems_1 if not systems_2: raise ValueError( @@ -104,7 +104,7 @@ def validate(self, if not environment_1: return - self.bundle.state[Errors.KEY_ENVIRONMENT_VARIABLE] = environment_1 + self.bundle[Errors.KEY_ENVIRONMENT_VARIABLE] = environment_1 if not environment_2: raise ValueError( @@ -133,7 +133,7 @@ def validate( if not pythons_1: return - self.bundle.state[Errors.KEY_PYTHON_1] = python_1 + self.bundle[Errors.KEY_PYTHON_1] = python_1 if not pythons_2: raise ValueError( @@ -206,7 +206,7 @@ def validate( if not modules_1: return - self.bundle.state[Errors.KEY_MODULES_1] = modules_1 + self.bundle[Errors.KEY_MODULES_1] = modules_1 if not module_2: raise ValueError( @@ -217,8 +217,8 @@ def validate( for module_1 in modules_1: - self.bundle.state[Errors.KEY_MODULE_NAME] = module_1.filename - self.bundle.state[Errors.KEY_MODULE_VERSION] = module_1.version + self.bundle[Errors.KEY_MODULE_NAME] = module_1.filename + self.bundle[Errors.KEY_MODULE_VERSION] = module_1.version if module_1.filename and module_1.filename != module_2.filename: raise ValueError( @@ -317,7 +317,7 @@ def validate( class VariableValidator: def __init__(self): - + self.logger = LogManager().get_logger(self.__class__.__name__) def validate( @@ -326,7 +326,7 @@ def validate( variables_2: List[Variable], contract_violation: BaseContractViolation ): - + bundle: Bundle = contract_violation.bundle self.logger.debug(f"Type of variables_1: {type(variables_1)}") self.logger.debug( Constants.LOG_MESSAGE_TEMPLATE.format( @@ -345,6 +345,8 @@ def validate( ) ) return + + bundle[Errors.KEY_VARIABLES_1] = variables_1 if not variables_2: self.logger.debug( @@ -354,29 +356,30 @@ def validate( details="No actual Variables found for validation" ) ) - raise ValueError(contract_violation.missing_error_handler(variables_1)) + raise ValueError(contract_violation.missing_error_handler()) - for vars_1 in variables_1: + for var_1 in variables_1: self.logger.debug( Constants.LOG_MESSAGE_TEMPLATE.format( operation="Variable validating", status="Progress", - details=f"Current vars_1: {vars_1}" + details=f"Current var_1: {var_1}" ) ) - if vars_1.name not in {var.name for var in variables_2}: - raise ValueError(contract_violation.missing_error_handler(vars_1.name)) + bundle[Errors.VARIABLES_DINAMIC_PAYLOAD[contract_violation.context]] = var_1.name + if var_1.name not in {var.name for var in variables_2}: + raise ValueError(contract_violation.missing_error_handler(var_1.name)) - for vars_1 in variables_1: - vars_2 = next((var for var in variables_2 if var.name == vars_1.name), None) - if not vars_2: - raise ValueError(contract_violation.missing_error_handler(vars_1.name)) + for var_1 in variables_1: + var_2 = next((var for var in variables_2 if var.name == var_1.name), None) + if not var_2: + raise ValueError(contract_violation.missing_error_handler()) - if vars_1.annotation and vars_1.annotation != vars_2.annotation: - raise ValueError(contract_violation.mismatch_error_handler(vars_1.annotation, vars_2.annotation)) + if var_1.annotation and var_1.annotation != var_2.annotation: + raise ValueError(contract_violation.mismatch_error_handler(var_1.annotation, var_2.annotation)) - if vars_1.value != vars_2.value: - raise ValueError(contract_violation.mismatch_error_handler(vars_1.value, vars_2.value)) + if var_1.value != var_2.value: + raise ValueError(contract_violation.mismatch_error_handler(var_1.value, var_2.value)) class FunctionValidator: diff --git a/src/importspy/violation_systems.py b/src/importspy/violation_systems.py index a164e75..b4ff473 100644 --- a/src/importspy/violation_systems.py +++ b/src/importspy/violation_systems.py @@ -8,6 +8,8 @@ field ) +from .violation_systems import Bundle + from typing import ( Optional, Any @@ -46,9 +48,10 @@ def invalid_error_handler(self) -> str: class BaseContractViolation(ContractViolation): - def __init__(self, context): + def __init__(self, context, bundle:Bundle): self._context = context + self.bundle = bundle super().__init__() @property @@ -67,65 +70,71 @@ def invalid_error_handler(self, allowed:Any, found:Any) -> str: class VariableContractViolation(BaseContractViolation): def __init__(self, scope:str, context:str, bundle:'Bundle'): - super().__init__(context) + super().__init__(context, bundle) self.scope = scope - self.bundle = bundle @property def label(self) -> str: - return Errors.VARIABLES_LABEL_TEMPLATE[self.scope][self.context].format(**self.bundle.state) + return Errors.VARIABLES_LABEL_TEMPLATE[self.scope][self.context].format(**self.bundle) class FunctionContractViolation(BaseContractViolation): def __init__(self, context:str, bundle:'Bundle'): - super().__init__(context) - self.bundle = bundle + super().__init__(context, bundle) @property def label(self) -> str: - return Errors.FUNCTIONS_LABEL_TEMPLATE[self.context].format(**self.bundle.state) + return Errors.FUNCTIONS_LABEL_TEMPLATE[self.context].format(**self.bundle) class RuntimeContractViolation(BaseContractViolation): def __init__(self, context:str, bundle:'Bundle'): - super().__init__(context) - self.bundle = bundle + super().__init__(context, bundle) @property def label(self) -> str: - return self.bundle.state[Errors.KEY_RUNTIMES_1] + return self.bundle[Errors.KEY_RUNTIMES_1] class SystemContractViolation(BaseContractViolation): def __init__(self, context:str, bundle:'Bundle'): - super().__init__(context) - self.bundle = bundle + super().__init__(context, bundle) @property def label(self) -> str: - return self.bundle.state[Errors.KEY_SYSTEMS_1] + return self.bundle[Errors.KEY_SYSTEMS_1] class PythonContractViolation(BaseContractViolation): def __init__(self, context:str, bundle:'Bundle'): - super().__init__(context) - self.bundle = bundle + super().__init__(context, bundle) @property def label(self) -> str: - return self.bundle.state[Errors.KEY_PYTHON_1] + return self.bundle[Errors.KEY_PYTHON_1] class ModuleContractViolation(BaseContractViolation): def __init__(self, context:str, bundle:'Bundle'): - super().__init__(context) - self.bundle = bundle + super().__init__(context, bundle) @property def label(self) -> str: - return Errors.MODULE_LABEL_TEMPLATE[self.context].format(**self.bundle.state) + return Errors.MODULE_LABEL_TEMPLATE[self.context].format(**self.bundle) @dataclass class Bundle: - state: Optional[dict[str, Any]] = field(default_factory=dict) \ No newline at end of file + state: Optional[dict[str, Any]] = field(default_factory=dict) + + def __getitem__(self, key): + return self.state[key] + + def __len__(self): + return len(self.state) + + def __setitem__(self, key, value): + self.state[key] = value + + def __repr__(self): + return repr(self.state) \ No newline at end of file From 1c53c7b3fce9d50ea3841fd32da431af72ec60d0 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Fri, 30 May 2025 16:37:55 +0200 Subject: [PATCH 15/40] refactor(errors): introduce spec-based label formatting for contract violations - Added support for `ENTITY_MESSAGES` and `COLLECTIONS_MESSAGES` in label templates - Updated all error label methods to accept a `spec` parameter for granular formatting - Standardized label generation across runtime, system, python, module, and function validators - Improved consistency in error message templates for collections and single entities --- src/importspy/constants.py | 100 ++++++++++++++++++++++++----- src/importspy/validators.py | 57 ++++++++++------ src/importspy/violation_systems.py | 44 ++++++------- 3 files changed, 141 insertions(+), 60 deletions(-) diff --git a/src/importspy/constants.py b/src/importspy/constants.py index 04b442f..c614560 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -105,6 +105,9 @@ class Errors: SCOPE_VARIABLE = "variable" SCOPE_ARGUMENT = "argument" + ENTITY_MESSAGES = "entity" + COLLECTIONS_MESSAGES = "collections" + CONTEXT_INTRO = { Contexts.RUNTIME_CONTEXT: "Runtime constraint violation", Contexts.ENVIRONMENT_CONTEXT: "Environment validation failure", @@ -118,26 +121,101 @@ class Category(str, Enum): MISMATCH = "mismatch" INVALID = "invalid" + RUNTIME_LABEL_TEMPLATE = { + + ENTITY_MESSAGES: 'The runtime "{runtime_1}"', + COLLECTIONS_MESSAGES: 'The runtimes "{runtimes_1}"' + + } + + SYSTEM_LABEL_TEMPLATE = { + + ENTITY_MESSAGES: 'The system "{system_1}"', + COLLECTIONS_MESSAGES: 'The systems "{systems_1}"' + + } + + PYTHON_LABEL_TEMPLATE = { + + ENTITY_MESSAGES: 'The python "{python_1}"', + COLLECTIONS_MESSAGES: 'The pythons "{pythons_1}"' + + } + VARIABLES_LABEL_TEMPLATE = { SCOPE_VARIABLE: { - Contexts.ENVIRONMENT_CONTEXT: 'The environment variable "{environment_variable_name}"', - Contexts.MODULE_CONTEXT: 'The variable "{variable_name}" in module "{module_name}"', - Contexts.CLASS_CONTEXT: 'The {attribute_type} attribute "{attribute_name}" in class "{class_name}"' + + ENTITY_MESSAGES: { + + Contexts.ENVIRONMENT_CONTEXT: 'The environment variable "{environment_variable_name}"', + Contexts.MODULE_CONTEXT: 'The variable "{variable_name}" in module "{module_name}"', + Contexts.CLASS_CONTEXT: 'The {attribute_type} attribute "{attribute_name}" in class "{class_name}"' + + }, + + COLLECTIONS_MESSAGES: { + + Contexts.ENVIRONMENT_CONTEXT: 'The environment "{environment_1}"', + Contexts.MODULE_CONTEXT: 'The variables "{variables_1}"', + Contexts.CLASS_CONTEXT: 'The attributes "{attrs_1}"' + + } }, - SCOPE_ARGUMENT: { - Contexts.MODULE_CONTEXT: 'The argument "{argument_name}" of function "{function_name}"', - Contexts.CLASS_CONTEXT: 'The argument "{argument_name}" of method "{method_name}" in class "{class_name}"', + + ENTITY_MESSAGES: { + + SCOPE_ARGUMENT: { + + Contexts.MODULE_CONTEXT: 'The argument "{argument_name}" of function "{function_name}"', + Contexts.CLASS_CONTEXT: 'The argument "{argument_name}" of method "{method_name}" in class "{class_name}"', + + }, + + COLLECTIONS_MESSAGES: { + + Contexts.MODULE_CONTEXT: 'The arguments "{arguments_1}" of function "{function_name}"', + Contexts.CLASS_CONTEXT: 'The arguments "{arguments_1}" of method "{method_name}", in class "{class_name}"' + + } + } + + } + + FUNCTIONS_LABEL_TEMPLATE = { + + ENTITY_MESSAGES: { + + Contexts.MODULE_CONTEXT: 'The function "{function_name}" in module "{filename}"', + Contexts.CLASS_CONTEXT: 'The method "{method_name}" in class "{class_name}"' + + }, + + COLLECTIONS_MESSAGES: { + + Contexts.MODULE_CONTEXT: 'The functions "{functions_1}" in module "{filename}"', + Contexts.CLASS_CONTEXT: 'The methods "{methods_1}" in class "{class_name}"' + + } + + } + + MODULE_LABEL_TEMPLATE = { + Contexts.RUNTIME_CONTEXT: 'The module "{filename}"', + Contexts.ENVIRONMENT_CONTEXT: 'The version "{version}" of module "{filename}"' } KEY_RUNTIMES_1 = "runtimes_1" KEY_SYSTEMS_1 = "runtimes_1" + KEY_PYTHONS_1 = "pythons_1" KEY_PYTHON_1 = "python_1" KEY_ENVIRONMENT_1 = "environment_1" KEY_ENVIRONMENT_VARIABLE_NAME = "environment_variable_name" KEY_MODULES_1 = "modules_1" KEY_VARIABLES_1 = "variables_1" + KEY_FUNCTIONS_1 = "functions_1" + KEY_VARIABLE_NAME = "variable_name" KEY_ARGUMENT_NAME = "argument_name" KEY_FUNCTION_NAME = "function_name" @@ -168,16 +246,6 @@ class Category(str, Enum): Contexts.CLASS_CONTEXT: KEY_METHOD_NAME } - FUNCTIONS_LABEL_TEMPLATE = { - Contexts.MODULE_CONTEXT: 'The function "{function_name}" in module "{filename}"', - Contexts.CLASS_CONTEXT: 'The method "{method_name}" in class "{class_name}"' - } - - MODULE_LABEL_TEMPLATE = { - Contexts.RUNTIME_CONTEXT: 'The module "{filename}"', - Contexts.ENVIRONMENT_CONTEXT: 'The version "{version}" of module "{filename}"' - } - ERROR_MESSAGE_TEMPLATES = { Category.MISSING: { TEMPLATE_KEY: "{label} is declared but missing.", diff --git a/src/importspy/validators.py b/src/importspy/validators.py index 8d62544..125d157 100644 --- a/src/importspy/validators.py +++ b/src/importspy/validators.py @@ -49,13 +49,18 @@ def validate( RuntimeContractViolation( Contexts.RUNTIME_CONTEXT, self.bundle - ).missing_error_handler()) + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) runtime_2 = runtimes_2[0] for runtime_1 in runtimes_1: if runtime_1.arch == runtime_2.arch: return runtime_1 + raise ValueError(RuntimeContractViolation( + Contexts.RUNTIME_CONTEXT, + self.bundle + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) + class SystemValidator: @@ -81,7 +86,7 @@ def validate( SystemContractViolation( Contexts.RUNTIME_CONTEXT, self.bundle - ).missing_error_handler()) + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) system_2 = systems_2[0] @@ -90,6 +95,11 @@ def validate( if system_1.environment: self._environment_validator.validate(system_1.environment, system_2.environment) return system_1.pythons + raise ValueError( + SystemContractViolation( + Contexts.RUNTIME_CONTEXT, + self.bundle + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) class EnvironmentValidator: @@ -104,14 +114,14 @@ def validate(self, if not environment_1: return - self.bundle[Errors.KEY_ENVIRONMENT_VARIABLE] = environment_1 + self.bundle[Errors.KEY_ENVIRONMENT_1] = environment_1 if not environment_2: raise ValueError( VariableContractViolation( Contexts.ENVIRONMENT_CONTEXT, self.bundle - ).missing_error_handler()) + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) variables_2 = environment_2.variables @@ -133,14 +143,14 @@ def validate( if not pythons_1: return - self.bundle[Errors.KEY_PYTHON_1] = python_1 + self.bundle[Errors.KEY_PYTHONS_1] = pythons_1 if not pythons_2: raise ValueError( PythonContractViolation( Contexts.RUNTIME_CONTEXT, self.bundle - ).missing_error_handler()) + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) python_2 = pythons_2[0] for python_1 in pythons_1: @@ -176,6 +186,7 @@ def _is_python_match( - If only interpreter is defined: match interpreter. - If none are defined: match anything (default `True`). """ + self.bundle[Errors.KEY_PYTHON_1] = python_1 if python_1.version and python_1.interpreter: return ( python_1.version == python_2.version and @@ -188,7 +199,11 @@ def _is_python_match( if python_1.interpreter: return python_1.interpreter == python_2.interpreter - return True + raise ValueError( + PythonContractViolation( + Contexts.RUNTIME_CONTEXT, + self.bundle + ).missing_error_handler(Errors.ENTITY_MESSAGES)) class ModuleValidator: @@ -213,7 +228,7 @@ def validate( ModuleContractViolation( Contexts.RUNTIME_CONTEXT, self.bundle - ).missing_error_handler()) + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) for module_1 in modules_1: @@ -225,14 +240,14 @@ def validate( ModuleContractViolation( Contexts.RUNTIME_CONTEXT, self.bundle - ).mismatch_error_handler(module_1.filename, module_2.filename)) + ).mismatch_error_handler(module_1.filename, module_2.filename, Errors.ENTITY_MESSAGES)) if module_1.version and module_1.version != module_2.version: raise ValueError( ModuleContractViolation( Contexts.RUNTIME_CONTEXT, self.bundle - ).mismatch_error_handler(module_1.version, module_2.version)) + ).mismatch_error_handler(module_1.version, module_2.version, Errors.ENTITY_MESSAGES)) self.variable_validator.validate( module_1.variables, @@ -356,7 +371,7 @@ def validate( details="No actual Variables found for validation" ) ) - raise ValueError(contract_violation.missing_error_handler()) + raise ValueError(contract_violation.missing_error_handler(Errors.COLLECTIONS_MESSAGES)) for var_1 in variables_1: self.logger.debug( @@ -368,18 +383,18 @@ def validate( ) bundle[Errors.VARIABLES_DINAMIC_PAYLOAD[contract_violation.context]] = var_1.name if var_1.name not in {var.name for var in variables_2}: - raise ValueError(contract_violation.missing_error_handler(var_1.name)) + raise ValueError(contract_violation.missing_error_handler(Errors.ENTITY_MESSAGES)) for var_1 in variables_1: var_2 = next((var for var in variables_2 if var.name == var_1.name), None) if not var_2: - raise ValueError(contract_violation.missing_error_handler()) + raise ValueError(contract_violation.missing_error_handler(Errors.ENTITY_MESSAGES)) if var_1.annotation and var_1.annotation != var_2.annotation: - raise ValueError(contract_violation.mismatch_error_handler(var_1.annotation, var_2.annotation)) + raise ValueError(contract_violation.mismatch_error_handler(var_1.annotation, var_2.annotation, Errors.ENTITY_MESSAGES)) if var_1.value != var_2.value: - raise ValueError(contract_violation.mismatch_error_handler(var_1.value, var_2.value)) + raise ValueError(contract_violation.mismatch_error_handler(var_1.value, var_2.value, Errors.ENTITY_MESSAGES)) class FunctionValidator: @@ -394,7 +409,8 @@ def validate( functions_2: List[Function], contract_violation: BaseContractViolation ): - + + bundle: Bundle = contract_violation.bundle self.logger.debug( Constants.LOG_MESSAGE_TEMPLATE.format( operation="Function validating", @@ -412,7 +428,7 @@ def validate( ) ) return - + bundle[Errors.KEY_FUNCTIONS_1] = functions_1 if not functions_2: self.logger.debug( Constants.LOG_MESSAGE_TEMPLATE.format( @@ -421,9 +437,10 @@ def validate( details="No actual functions found" ) ) - raise ValueError(contract_violation.missing_error_handler(functions_1)) + raise ValueError(contract_violation.missing_error_handler(Errors.COLLECTIONS_MESSAGES)) for function_1 in functions_1: + bundle[Errors.FUNCTIONS_DINAMIC_PAYLOAD[contract_violation.context]] = function_1.name self.logger.debug( Constants.LOG_MESSAGE_TEMPLATE.format( operation="Function validating", @@ -439,12 +456,12 @@ def validate( details=f"function_1: {function_1}; functions_2: {functions_2}" ) ) - raise ValueError(contract_violation.missing_error_handler(function_1)) + raise ValueError(contract_violation.missing_error_handler(Errors.ENTITY_MESSAGES)) for function_1 in functions_1: function_2 = next((f for f in functions_2 if f.name == function_1.name), None) if not function_2: - raise ValueError(contract_violation.missing_error_handler/function_1) + raise ValueError(contract_violation.missing_error_handler(Errors.ENTITY_MESSAGES)) self.argument_validator.validate( function_1.arguments, diff --git a/src/importspy/violation_systems.py b/src/importspy/violation_systems.py index b4ff473..fac33e6 100644 --- a/src/importspy/violation_systems.py +++ b/src/importspy/violation_systems.py @@ -29,21 +29,20 @@ def context(self) -> str: def category(self) -> str: pass - @property @abstractmethod - def label(self) -> str: + def label(self, spec:str) -> str: pass @abstractmethod - def missing_error_handler(self) -> str: + def missing_error_handler(self, spec:str) -> str: pass @abstractmethod - def mismatch_error_handler(self) -> str: + def mismatch_error_handler(self, spec:str) -> str: pass @abstractmethod - def invalid_error_handler(self) -> str: + def invalid_error_handler(self, spec:str) -> str: pass class BaseContractViolation(ContractViolation): @@ -58,14 +57,14 @@ def __init__(self, context, bundle:Bundle): def context(self) -> str: return self._context - def missing_error_handler(self) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label).capitalize()}' + def missing_error_handler(self, spec:str) -> str: + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][Errors.TEMPLATE_KEY].format(label=self.label(spec))} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label(spec)).capitalize()}' - def mismatch_error_handler(self, expected:Any, actual:Any) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label, expected=expected, actual=actual).capitalize()}' + def mismatch_error_handler(self, expected:Any, actual:Any, spec:str) -> str: + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][Errors.TEMPLATE_KEY].format(label=self.label(spec))} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label(spec), expected=expected, actual=actual).capitalize()}' - def invalid_error_handler(self, allowed:Any, found:Any) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][Errors.TEMPLATE_KEY].format(label=self.label)} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label, expected=allowed, actual=found).capitalize()}' + def invalid_error_handler(self, allowed:Any, found:Any, spec:str) -> str: + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][Errors.TEMPLATE_KEY].format(label=self.label(spec))} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label(spec), expected=allowed, actual=found).capitalize()}' class VariableContractViolation(BaseContractViolation): @@ -82,27 +81,24 @@ class FunctionContractViolation(BaseContractViolation): def __init__(self, context:str, bundle:'Bundle'): super().__init__(context, bundle) - @property - def label(self) -> str: - return Errors.FUNCTIONS_LABEL_TEMPLATE[self.context].format(**self.bundle) + def label(self, spec:str) -> str: + return Errors.FUNCTIONS_LABEL_TEMPLATE[spec][self.context].format(**self.bundle) class RuntimeContractViolation(BaseContractViolation): def __init__(self, context:str, bundle:'Bundle'): super().__init__(context, bundle) - @property - def label(self) -> str: - return self.bundle[Errors.KEY_RUNTIMES_1] + def label(self, spec:str) -> str: + return Errors.RUNTIME_LABEL_TEMPLATE[spec] class SystemContractViolation(BaseContractViolation): def __init__(self, context:str, bundle:'Bundle'): super().__init__(context, bundle) - @property - def label(self) -> str: - return self.bundle[Errors.KEY_SYSTEMS_1] + def label(self, spec:str) -> str: + return Errors.SYSTEM_LABEL_TEMPLATE[spec] class PythonContractViolation(BaseContractViolation): @@ -110,8 +106,8 @@ def __init__(self, context:str, bundle:'Bundle'): super().__init__(context, bundle) @property - def label(self) -> str: - return self.bundle[Errors.KEY_PYTHON_1] + def label(self, spec:str) -> str: + return Errors.PYTHON_LABEL_TEMPLATE[spec] class ModuleContractViolation(BaseContractViolation): @@ -119,8 +115,8 @@ def __init__(self, context:str, bundle:'Bundle'): super().__init__(context, bundle) @property - def label(self) -> str: - return Errors.MODULE_LABEL_TEMPLATE[self.context].format(**self.bundle) + def label(self, spec:str) -> str: + return Errors.MODULE_LABEL_TEMPLATE[spec][self.context].format(**self.bundle) @dataclass class Bundle: From e879f81aa2fd95850c9493e1aaca2ab71c2e3b20 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Sat, 31 May 2025 16:04:11 +0200 Subject: [PATCH 16/40] feat(validators): add contextualized class validation and superclass recursion - Introduced collection-aware error labels for class validation - Extended ClassValidator to support recursive validation of superclasses - Updated MODULE_LABEL_TEMPLATE with support for ENTITY and COLLECTION specs - Improved superclass extraction logic to return full class info - Added KEY_CLASSES_1 constant for consistent bundle usage --- src/importspy/constants.py | 18 +++- src/importspy/models.py | 6 +- src/importspy/utilities/module_util.py | 32 ++++--- src/importspy/validators.py | 117 +++++++++++++++---------- 4 files changed, 111 insertions(+), 62 deletions(-) diff --git a/src/importspy/constants.py b/src/importspy/constants.py index c614560..20e1aba 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -202,8 +202,21 @@ class Category(str, Enum): } MODULE_LABEL_TEMPLATE = { - Contexts.RUNTIME_CONTEXT: 'The module "{filename}"', - Contexts.ENVIRONMENT_CONTEXT: 'The version "{version}" of module "{filename}"' + + ENTITY_MESSAGES: { + + Contexts.CLASS_CONTEXT: 'The class "{class_name}"', + Contexts.RUNTIME_CONTEXT: 'The module "{filename}"', + Contexts.ENVIRONMENT_CONTEXT: 'The version "{version}" of module "{filename}"' + + }, + + COLLECTIONS_MESSAGES: { + + Contexts.CLASS_CONTEXT: 'The classes "{classes_1}" in module "{filename}"' + + } + } KEY_RUNTIMES_1 = "runtimes_1" @@ -215,6 +228,7 @@ class Category(str, Enum): KEY_MODULES_1 = "modules_1" KEY_VARIABLES_1 = "variables_1" KEY_FUNCTIONS_1 = "functions_1" + KEY_CLASSES_1 = "classes_1" KEY_VARIABLE_NAME = "variable_name" KEY_ARGUMENT_NAME = "argument_name" diff --git a/src/importspy/models.py b/src/importspy/models.py index f7d10b2..af68dd4 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -165,7 +165,7 @@ class Class(BaseModel): name: str attributes: Optional[list[Attribute]] = None methods: Optional[list[Function]] = None - superclasses: Optional[list[str]] = None + superclasses: Optional[list['Class']] = None @classmethod def from_class_info(cls, extracted_classes: list[ClassInfo]): @@ -276,8 +276,9 @@ class Error(BaseModel): description: str solution: str +""" @classmethod - def from_contract_violation(cls, contract_violation: 'ContractViolation'): + def from_contract_violation(cls, contract_violation: BaseContractViolation): tpl = Errors.ERROR_MESSAGE_TEMPLATES.get(contract_violation.category) title = Errors.CONTEXT_INTRO.get(contract_violation.context) description = tpl[Errors.TEMPLATE_KEY].format(label=contract_violation.label) @@ -292,6 +293,7 @@ def from_contract_violation(cls, contract_violation: 'ContractViolation'): def render_message(self) -> str: return f"[{self.title}] {self.description} {self.solution}" +""" diff --git a/src/importspy/utilities/module_util.py b/src/importspy/utilities/module_util.py index c81b419..bf9d440 100644 --- a/src/importspy/utilities/module_util.py +++ b/src/importspy/utilities/module_util.py @@ -253,21 +253,25 @@ def extract_classes(self, info_module: ModuleType) -> List[ClassInfo]: for name, cls in inspect.getmembers(info_module, inspect.isclass): attributes = self.extract_attributes(cls, info_module) methods = self.extract_methods(cls) - superclasses = [base.__name__ for base in cls.__bases__ if base.__name__ != "object"] + superclasses = self.extract_superclasses(cls) classes.append(ClassInfo(name, attributes, methods, superclasses)) return classes - def extract_superclasses(self, module: ModuleType) -> List[str]: - """ - Extracts unique superclass names from all classes. - Returns: - -------- - List[str] - """ - superclasses = set() - for name, cls in inspect.getmembers(module, inspect.isclass): - if cls.__module__ == module.__name__: - for base in cls.__bases__: - superclasses.add(base.__name__) - return list(superclasses) + def extract_superclasses(self, cls) -> List[ClassInfo]: + superclasses = [] + for base in cls.__bases__: + if base.__name__ == "object": + continue + module = sys.modules.get(base.__module__) + if not module: + continue + superclasses.append(ClassInfo( + base.__name__, + self.extract_attributes(base, module), + self.extract_methods(base), + [] + )) + return superclasses + + diff --git a/src/importspy/validators.py b/src/importspy/validators.py index 125d157..62b85d0 100644 --- a/src/importspy/validators.py +++ b/src/importspy/validators.py @@ -27,6 +27,8 @@ Errors ) +from .config import Config + from .log_manager import LogManager class RuntimeValidator: @@ -270,7 +272,11 @@ def validate( self.class_validator.validate( module_1.classes, - module_2.classes + module_2.classes, + ModuleContractViolation( + Contexts.CLASS_CONTEXT, + self.bundle + ) ) @@ -284,50 +290,73 @@ def __init__(self): def validate( self, classes_1: List[Class], - classes_2: List[Class] - ): - if classes_1: - for class_1 in classes_1: - class_2 = next((cls for cls in classes_2 if cls.name == class_1.name), None) - if not class_2: - raise ValueError(Errors.CLASS_MISSING.format(class_1.name)) - - self.variable_validator.validate( - class_1.get_class_attributes(), - class_2.get_class_attributes(), - VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.CLASS_CONTEXT, ClassBundle(attribute_type="class", class_name=class_1.name)) - ) + classes_2: List[Class], + contract_violation: BaseContractViolation + ): + if not classes_1: + return + + bundle: Bundle = contract_violation.bundle + bundle[Errors.KEY_CLASSES_1] = classes_1 - self.variable_validator.validate( - class_1.get_instance_attributes(), - class_2.get_instance_attributes(), - VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.CLASS_CONTEXT, ClassBundle(attribute_type="instance", class_name=class_1.name)) - ) + if not classes_2: + raise ValueError( + ModuleContractViolation( + Contexts.CLASS_CONTEXT, + bundle + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) + + for class_1 in classes_1: + class_2 = next((cls for cls in classes_2 if cls.name == class_1.name), None) + + bundle[Errors.KEY_CLASS_NAME] = class_1.name - self.function_validator.validate( - class_1.methods, - class_2.methods, - FunctionContractViolation( + if not class_2: + raise ValueError( + ModuleContractViolation( Contexts.CLASS_CONTEXT, - ClassBundle( - class_name=class_1.name - ) - ) + bundle + ).missing_error_handler(Errors.ENTITY_MESSAGES) ) + + bundle[Errors.KEY_ATTRIBUTE_TYPE] = Config.CLASS_TYPE - self._function_validator.validate( - class_1.methods, - class_2.methods, - classname=class_1.name - ) + self.variable_validator.validate( + class_1.get_class_attributes(), + class_2.get_class_attributes(), + VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.CLASS_CONTEXT, bundle) + ) - CommonValidator().list_validate( - class_1.superclasses, - class_2.superclasses, - Errors.CLASS_SUPERCLASS_MISSING, - class_2.name + bundle[Errors.KEY_ATTRIBUTE_TYPE] = Config.INSTANCE_TYPE + + self.variable_validator.validate( + class_1.get_instance_attributes(), + class_2.get_instance_attributes(), + VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.CLASS_CONTEXT, bundle) + ) + + self.function_validator.validate( + class_1.methods, + class_2.methods, + FunctionContractViolation( + Contexts.CLASS_CONTEXT, + bundle ) - return + ) + + self.function_validator.validate( + class_1.methods, + class_2.methods, + classname=class_1.name + ) + + self.validate(class_1.superclasses, + class_2.superclasses, + ModuleContractViolation( + Contexts.CLASS_CONTEXT, + bundle + ) + ) class VariableValidator: @@ -428,7 +457,9 @@ def validate( ) ) return + bundle[Errors.KEY_FUNCTIONS_1] = functions_1 + if not functions_2: self.logger.debug( Constants.LOG_MESSAGE_TEMPLATE.format( @@ -467,15 +498,13 @@ def validate( function_1.arguments, function_2.arguments, VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.MODULE_CONTEXT) - ) if function_1.return_annotation and function_1.return_annotation != function_2.return_annotation: - raise ValueError( - Errors.FUNCTION_RETURN_ANNOTATION_MISMATCH.format( - function_1.name, - function_1.return_annotation, - function_2.return_annotation + raise ValueError(contract_violation.mismatch_error_handler( + function_1.return_annotation, + function_2.return_annotation, + Errors.ENTITY_MESSAGES ) ) From 1ec20290c335e137f3c405a7e60f9954e4f5b2bd Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Sun, 1 Jun 2025 12:34:56 +0200 Subject: [PATCH 17/40] refactor(validators): unify variable validation across arguments and attributes - Replaced ArgumentValidator and AttributeValidator with a unified VariableValidator - Refactored argument and attribute tests to use VariableValidator with contextual contract violations - Introduced enum-based SupportedArchitectures and SupportedOS for stricter typing - Fixed circular import in violation_systems - Extended test coverage with bundle-aware fixtures for validation contexts --- src/importspy/constants.py | 4 +- src/importspy/validators.py | 5 +- src/importspy/violation_systems.py | 4 +- tests/conftest.py | 24 +++++++ tests/validators/test_argument_validator.py | 69 ++++++++++++-------- tests/validators/test_attribute_validator.py | 43 ++++++++++-- 6 files changed, 111 insertions(+), 38 deletions(-) create mode 100644 tests/conftest.py diff --git a/src/importspy/constants.py b/src/importspy/constants.py index 20e1aba..a4d194c 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -15,7 +15,7 @@ class Constants: considers valid and contract-compliant. """ - class SupportedArchitectures: + class SupportedArchitectures(str, Enum): Config.ARCH_x86_64 Config.ARCH_AARCH64 @@ -26,7 +26,7 @@ class SupportedArchitectures: Config.ARCH_PPC64LE Config.ARCH_S390X - class SupportedOS: + class SupportedOS(str, Enum): Config.OS_WINDOWS Config.OS_LINUX diff --git a/src/importspy/validators.py b/src/importspy/validators.py index 62b85d0..d2a9215 100644 --- a/src/importspy/validators.py +++ b/src/importspy/validators.py @@ -497,7 +497,10 @@ def validate( self.argument_validator.validate( function_1.arguments, function_2.arguments, - VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.MODULE_CONTEXT) + VariableContractViolation( + Errors.SCOPE_ARGUMENT, + Contexts.MODULE_CONTEXT, + bundle) ) if function_1.return_annotation and function_1.return_annotation != function_2.return_annotation: diff --git a/src/importspy/violation_systems.py b/src/importspy/violation_systems.py index fac33e6..9858c8b 100644 --- a/src/importspy/violation_systems.py +++ b/src/importspy/violation_systems.py @@ -8,8 +8,6 @@ field ) -from .violation_systems import Bundle - from typing import ( Optional, Any @@ -47,7 +45,7 @@ def invalid_error_handler(self, spec:str) -> str: class BaseContractViolation(ContractViolation): - def __init__(self, context, bundle:Bundle): + def __init__(self, context, bundle:'Bundle'): self._context = context self.bundle = bundle diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..68d68e5 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,24 @@ +import pytest + +from importspy.violation_systems import Bundle + +from importspy.constants import ( + Errors, + Contexts +) + +@pytest.fixture +def modulebundle() -> Bundle: + bundle = Bundle() + bundle[Errors.KEY_MODULE_NAME] = "testmodule.py" + bundle[Errors.KEY_MODULE_VERSION] = "0.1.0" + return bundle + +@pytest.fixture +def classbundle(modulebundle) -> Bundle: + modulebundle[Errors.KEY_CLASS_NAME] = "TestClass" + return modulebundle + +@pytest.fixture +def functionbundle(modulebundle) -> Bundle: + modulebundle[Errors.FUNCTIONS_DINAMIC_PAYLOAD[Contexts.MODULE_CONTEXT]] = "test_function" \ No newline at end of file diff --git a/tests/validators/test_argument_validator.py b/tests/validators/test_argument_validator.py index 37e0681..ca580b0 100644 --- a/tests/validators/test_argument_validator.py +++ b/tests/validators/test_argument_validator.py @@ -3,17 +3,28 @@ Argument ) from typing import List -from importspy.validators.argument_validator import ArgumentValidator -from importspy.errors import Errors +from importspy.validators import VariableValidator import re -from importspy.constants import Constants + +from importspy.constants import ( + Constants, + Errors, + Contexts +) + +from importspy.violation_systems import ( + VariableContractViolation, + Bundle +) + +from importspy.config import Config class TestArgumentValidator: - validator = ArgumentValidator() + validator = VariableValidator() @pytest.fixture - def data_1(self): + def data_1(self) -> Argument: return [Argument( name="arg1", annotation="int", @@ -21,7 +32,7 @@ def data_1(self): )] @pytest.fixture - def data_2(self): + def data_2(self) -> Argument: return [Argument( name="arg2", annotation="str", @@ -29,7 +40,7 @@ def data_2(self): )] @pytest.fixture - def data_3(self): + def data_3(self) -> Argument: return [Argument( name="arg1", annotation="int", @@ -37,13 +48,17 @@ def data_3(self): )] @pytest.fixture - def data_4(self): + def data_4(self) -> Argument: return [Argument( name="arg1", annotation="int", value=10 )] - + + @pytest.fixture + def contract_violation(self): + return VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.MODULE_CONTEXT) + @pytest.fixture def argument_value_setter(self, data_3: List[Argument]): data_3[0].value = 10 @@ -52,17 +67,17 @@ def argument_value_setter(self, data_3: List[Argument]): def argument_annotation_setter(self, data_3: List[Argument]): data_3[0].annotation = "str" - def test_argument_match(self, data_1: List[Argument], data_4: List[Argument]): - assert self.validator.validate(data_1, data_4, "function_name") + def test_argument_match(self, data_1: List[Argument], data_4: List[Argument], contract_violation): + assert self.validator.validate(data_1, data_4, contract_violation) @pytest.mark.usefixtures("argument_value_setter") - def test_argument_match_1(self, data_1: List[Argument], data_3: List[Argument]): - assert self.validator.validate(data_1, data_3, "function_name") + def test_argument_match_1(self, data_1: List[Argument], data_3: List[Argument], contract_violation): + assert self.validator.validate(data_1, data_3, contract_violation) - def test_argument_mismatch(self, data_2: List[Argument]): - assert self.validator.validate(None, data_2, "function_name") is None + def test_argument_mismatch(self, data_2: List[Argument], contract_violation): + assert self.validator.validate(None, data_2, contract_violation) is None - def test_argument_mismatch_1(self, data_3: List[Argument], data_4: List[Argument]): + def test_argument_mismatch_1(self, data_3: List[Argument], data_4: List[Argument], contract_violation): with pytest.raises(ValueError, match=re.escape( Errors.ARGUMENT_MISMATCH.format( @@ -72,40 +87,40 @@ def test_argument_mismatch_1(self, data_3: List[Argument], data_4: List[Argument data_3[0].value ) )): - self.validator.validate(data_4, data_3, "function_name") + self.validator.validate(data_4, data_3, contract_violation) - def test_argument_mismatch_2(self): - assert self.validator.validate(None, None, "function_name") is None + def test_argument_mismatch_2(self, contract_violation): + assert self.validator.validate(None, None, contract_violation) is None - def test_argument_mismatch_3(self, data_2: List[Argument]): - assert self.validator.validate(None, data_2, "function_name") is None + def test_argument_mismatch_3(self, data_2: List[Argument], contract_violation): + assert self.validator.validate(None, data_2, contract_violation) is None @pytest.mark.usefixtures("argument_value_setter") @pytest.mark.usefixtures("argument_annotation_setter") - def test_argument_mismatch_5(self, data_1: List[Argument], data_3: List[Argument]): + def test_argument_mismatch_5(self, data_1: List[Argument], data_3: List[Argument], contract_violation): arg_1 = data_3[0] arg_2 = data_1[0] with pytest.raises( ValueError, match=re.escape(Errors.ARGUMENT_MISMATCH.format(Constants.ANNOTATION, arg_1.name, arg_1.annotation, arg_2.annotation)) ): - self.validator.validate(data_3, data_1, "function_name") + self.validator.validate(data_3, data_1, contract_violation) - def test_argument_mismatch_6(self, data_1: List[Argument], data_3: List[Argument]): + def test_argument_mismatch_6(self, data_1: List[Argument], data_3: List[Argument], contract_violation): arg_1 = data_3[0] arg_2 = data_1[0] with pytest.raises( ValueError, match=re.escape(Errors.ARGUMENT_MISMATCH.format(Constants.VALUE, arg_1.name, arg_1.value, arg_2.value)) ): - self.validator.validate(data_3, data_1, "function_name") + self.validator.validate(data_3, data_1, contract_violation) - def test_argument_mismatch_7(self, data_1: List[Argument]): + def test_argument_mismatch_7(self, data_1: List[Argument], contract_violation): with pytest.raises( ValueError, match=re.escape( Errors.ELEMENT_MISSING.format(data_1) ) ): - self.validator.validate(data_1, None, "function_name") + self.validator.validate(data_1, None, contract_violation) diff --git a/tests/validators/test_attribute_validator.py b/tests/validators/test_attribute_validator.py index e8b5770..82c4cf0 100644 --- a/tests/validators/test_attribute_validator.py +++ b/tests/validators/test_attribute_validator.py @@ -2,16 +2,30 @@ from importspy.models import ( Attribute ) -from importspy.config import Config + +from importspy.validators import VariableValidator + from typing import List -from importspy.validators.attribute_validator import AttributeValidator -from importspy.errors import Errors + import re -from importspy.constants import Constants + +from importspy.constants import ( + Constants, + Errors, + Contexts +) + +from importspy.violation_systems import ( + VariableContractViolation, + Bundle +) + +from importspy.config import Config + class TestAttributeValidator: - validator = AttributeValidator() + validator = VariableValidator() @pytest.fixture def data_1(self): @@ -45,6 +59,25 @@ def data_4(self): value=4 )] + @pytest.fixture + def class_type_bundle(self, classbundle) -> Bundle: + classbundle[Errors.KEY_ATTRIBUTE_TYPE] = Config.CLASS_TYPE + return classbundle + + @pytest.fixture + def instance_type_bundle(self, classbundle) -> Bundle: + classbundle[Errors.KEY_ATTRIBUTE_TYPE] = Config.INSTANCE_TYPE + return classbundle + + @pytest.fixture + def class_type_contract(self, class_type_bundle): + return VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.CLASS_CONTEXT, class_type_bundle) + + @pytest.fixture + def instance_type_contract(self, instance_type_bundle): + return VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.CLASS_CONTEXT, instance_type_bundle) + + @pytest.fixture def attribute_value_setter(self, data_3:Attribute): data_3[0].value = "value" From 992fb59ce1a418d67900ddbe9d193d871a4fb037 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Sun, 1 Jun 2025 18:09:24 +0200 Subject: [PATCH 18/40] feat(validators): support dynamic spec dispatch for variable contract violations - Updated `VARIABLES_DINAMIC_PAYLOAD` to support nested ENTITY_MESSAGES and COLLECTIONS_MESSAGES - Refactored `VariableValidator` to dynamically resolve keys based on scope and context - Improved `Bundle` to implement MutableMapping for cleaner access and interoperability - Adjusted error formatting to remove redundant solution templates - Extended test coverage for argument mismatch scenarios using contextual spec selection --- src/importspy/config.py | 6 +- src/importspy/constants.py | 74 ++++++++++++------ src/importspy/validators.py | 7 +- src/importspy/violation_systems.py | 38 +++++----- tests/conftest.py | 3 +- tests/validators/test_argument_validator.py | 83 +++++++++++++-------- 6 files changed, 132 insertions(+), 79 deletions(-) diff --git a/src/importspy/config.py b/src/importspy/config.py index 663a1bb..d1da905 100644 --- a/src/importspy/config.py +++ b/src/importspy/config.py @@ -111,6 +111,6 @@ class Config: ANNOTATION_UNION = "Union" ANNOTATION_ANY = "Any" ANNOTATION_CALLABLE = "Callable" - ANNOTATION_LIST = "List" - ANNOTATION_DICT = "Dict" - ANNOTATION_TUPLE = "Tuple" + ANNOTATION_LIST_TYPING = "List" + ANNOTATION_DICT_TYPING = "Dict" + ANNOTATION_TUPLE_TYPING = "Tuple" diff --git a/src/importspy/constants.py b/src/importspy/constants.py index a4d194c..3dcb627 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -67,21 +67,21 @@ class SupportedClassAttributeTypes(str, Enum): INSTANCE_TYPE = Config.INSTANCE_TYPE class SupportedAnnotations(str, Enum): - Config.ANNOTATION_INT, - Config.ANNOTATION_FLOAT, - Config.ANNOTATION_STR, - Config.ANNOTATION_BOOL, - Config.ANNOTATION_LIST, - Config.ANNOTATION_DICT, - Config.ANNOTATION_TUPLE, - Config.ANNOTATION_SET, - Config.ANNOTATION_OPTIONAL, - Config.ANNOTATION_UNION, - Config.ANNOTATION_ANY, - Config.ANNOTATION_CALLABLE, - Config.ANNOTATION_LIST, - Config.ANNOTATION_DICT, - Config.ANNOTATION_TUPLE + INT = Config.ANNOTATION_INT + FLOAT = Config.ANNOTATION_FLOAT + STR = Config.ANNOTATION_STR + BOOL = Config.ANNOTATION_BOOL + LIST = Config.ANNOTATION_LIST + DICT = Config.ANNOTATION_DICT + TUPLE = Config.ANNOTATION_TUPLE + SET = Config.ANNOTATION_SET + OPTIONAL = Config.ANNOTATION_OPTIONAL + UNION = Config.ANNOTATION_UNION + ANY = Config.ANNOTATION_ANY + CALLABLE = Config.ANNOTATION_CALLABLE + LIST_TYPING = Config.ANNOTATION_LIST_TYPING + DICT_TYPING = Config.ANNOTATION_DICT_TYPING + TUPLE_TYPING = Config.ANNOTATION_TUPLE_TYPING LOG_MESSAGE_TEMPLATE = ( "[Operation: {operation}] [Status: {status}] " @@ -158,14 +158,14 @@ class Category(str, Enum): Contexts.ENVIRONMENT_CONTEXT: 'The environment "{environment_1}"', Contexts.MODULE_CONTEXT: 'The variables "{variables_1}"', - Contexts.CLASS_CONTEXT: 'The attributes "{attrs_1}"' + Contexts.CLASS_CONTEXT: 'The attributes "{attributes_1}"' } }, - ENTITY_MESSAGES: { + SCOPE_ARGUMENT: { - SCOPE_ARGUMENT: { + ENTITY_MESSAGES: { Contexts.MODULE_CONTEXT: 'The argument "{argument_name}" of function "{function_name}"', Contexts.CLASS_CONTEXT: 'The argument "{argument_name}" of method "{method_name}" in class "{class_name}"', @@ -227,6 +227,8 @@ class Category(str, Enum): KEY_ENVIRONMENT_VARIABLE_NAME = "environment_variable_name" KEY_MODULES_1 = "modules_1" KEY_VARIABLES_1 = "variables_1" + KEY_ATTRIBUTES_1 = "attributes_1" + KEY_ARGUMENTS_1 = "arguments_1" KEY_FUNCTIONS_1 = "functions_1" KEY_CLASSES_1 = "classes_1" @@ -244,13 +246,39 @@ class Category(str, Enum): VARIABLES_DINAMIC_PAYLOAD = { SCOPE_VARIABLE: { - Contexts.ENVIRONMENT_CONTEXT: KEY_ENVIRONMENT_VARIABLE_NAME, - Contexts.MODULE_CONTEXT: KEY_VARIABLE_NAME, - Contexts.CLASS_CONTEXT: KEY_ATTRIBUTE_NAME + + ENTITY_MESSAGES: { + + Contexts.ENVIRONMENT_CONTEXT: KEY_ENVIRONMENT_VARIABLE_NAME, + Contexts.MODULE_CONTEXT: KEY_VARIABLE_NAME, + Contexts.CLASS_CONTEXT: KEY_ATTRIBUTE_NAME + + }, + + COLLECTIONS_MESSAGES: { + + Contexts.ENVIRONMENT_CONTEXT: KEY_ENVIRONMENT_1, + Contexts.MODULE_CONTEXT: KEY_VARIABLES_1, + Contexts.CLASS_CONTEXT: KEY_ATTRIBUTES_1 + + } }, SCOPE_ARGUMENT: { - Contexts.MODULE_CONTEXT: KEY_ARGUMENT_NAME, - Contexts.CLASS_CONTEXT: KEY_ARGUMENT_NAME + + ENTITY_MESSAGES: { + + Contexts.MODULE_CONTEXT: KEY_ARGUMENT_NAME, + Contexts.CLASS_CONTEXT: KEY_ARGUMENT_NAME + + }, + + COLLECTIONS_MESSAGES: { + + Contexts.MODULE_CONTEXT: KEY_ARGUMENTS_1, + Contexts.CLASS_CONTEXT: KEY_ARGUMENTS_1 + + } + } } diff --git a/src/importspy/validators.py b/src/importspy/validators.py index d2a9215..ffc3488 100644 --- a/src/importspy/validators.py +++ b/src/importspy/validators.py @@ -368,7 +368,7 @@ def validate( self, variables_1: List[Variable], variables_2: List[Variable], - contract_violation: BaseContractViolation + contract_violation: VariableContractViolation ): bundle: Bundle = contract_violation.bundle self.logger.debug(f"Type of variables_1: {type(variables_1)}") @@ -390,7 +390,7 @@ def validate( ) return - bundle[Errors.KEY_VARIABLES_1] = variables_1 + bundle[Errors.VARIABLES_DINAMIC_PAYLOAD[contract_violation.scope][Errors.COLLECTIONS_MESSAGES][contract_violation.context]] = variables_1 if not variables_2: self.logger.debug( @@ -410,7 +410,8 @@ def validate( details=f"Current var_1: {var_1}" ) ) - bundle[Errors.VARIABLES_DINAMIC_PAYLOAD[contract_violation.context]] = var_1.name + bundle[Errors.VARIABLES_DINAMIC_PAYLOAD[contract_violation.scope][Errors.ENTITY_MESSAGES][contract_violation.context]] = var_1.name + self.logger.debug(bundle) if var_1.name not in {var.name for var in variables_2}: raise ValueError(contract_violation.missing_error_handler(Errors.ENTITY_MESSAGES)) diff --git a/src/importspy/violation_systems.py b/src/importspy/violation_systems.py index 9858c8b..3d8c3cd 100644 --- a/src/importspy/violation_systems.py +++ b/src/importspy/violation_systems.py @@ -3,6 +3,8 @@ abstractmethod ) +from collections.abc import MutableMapping + from dataclasses import ( dataclass, field @@ -10,7 +12,8 @@ from typing import ( Optional, - Any + Any, + Iterator ) from .constants import Errors @@ -22,11 +25,6 @@ class ContractViolation(ABC): def context(self) -> str: pass - @property - @abstractmethod - def category(self) -> str: - pass - @abstractmethod def label(self, spec:str) -> str: pass @@ -56,13 +54,13 @@ def context(self) -> str: return self._context def missing_error_handler(self, spec:str) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][Errors.TEMPLATE_KEY].format(label=self.label(spec))} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label(spec)).capitalize()}' + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][Errors.TEMPLATE_KEY].format(label=self.label(spec))} - {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][Errors.SOLUTION_KEY].capitalize()}' def mismatch_error_handler(self, expected:Any, actual:Any, spec:str) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][Errors.TEMPLATE_KEY].format(label=self.label(spec))} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label(spec), expected=expected, actual=actual).capitalize()}' + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][Errors.TEMPLATE_KEY].format(label=self.label(spec), expected=expected, actual=actual)} - {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][Errors.SOLUTION_KEY].capitalize()}' def invalid_error_handler(self, allowed:Any, found:Any, spec:str) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][Errors.TEMPLATE_KEY].format(label=self.label(spec))} - {Errors.ERROR_MESSAGE_TEMPLATES[self.category][Errors.SOLUTION_KEY].format(label=self.label(spec), expected=allowed, actual=found).capitalize()}' + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][Errors.TEMPLATE_KEY].format(label=self.label(spec), expected=allowed, actual=found)} - {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][Errors.SOLUTION_KEY].capitalize()}' class VariableContractViolation(BaseContractViolation): @@ -70,9 +68,8 @@ def __init__(self, scope:str, context:str, bundle:'Bundle'): super().__init__(context, bundle) self.scope = scope - @property - def label(self) -> str: - return Errors.VARIABLES_LABEL_TEMPLATE[self.scope][self.context].format(**self.bundle) + def label(self, spec:str) -> str: + return Errors.VARIABLES_LABEL_TEMPLATE[self.scope][spec][self.context].format(**self.bundle) class FunctionContractViolation(BaseContractViolation): @@ -117,18 +114,23 @@ def label(self, spec:str) -> str: return Errors.MODULE_LABEL_TEMPLATE[spec][self.context].format(**self.bundle) @dataclass -class Bundle: - +class Bundle(MutableMapping): state: Optional[dict[str, Any]] = field(default_factory=dict) def __getitem__(self, key): return self.state[key] - def __len__(self): - return len(self.state) - def __setitem__(self, key, value): self.state[key] = value - + + def __delitem__(self, key): + del self.state[key] + + def __iter__(self) -> Iterator: + return iter(self.state) + + def __len__(self) -> int: + return len(self.state) + def __repr__(self): return repr(self.state) \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 68d68e5..e9f715c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -21,4 +21,5 @@ def classbundle(modulebundle) -> Bundle: @pytest.fixture def functionbundle(modulebundle) -> Bundle: - modulebundle[Errors.FUNCTIONS_DINAMIC_PAYLOAD[Contexts.MODULE_CONTEXT]] = "test_function" \ No newline at end of file + modulebundle[Errors.FUNCTIONS_DINAMIC_PAYLOAD[Contexts.MODULE_CONTEXT]] = "test_function" + return modulebundle \ No newline at end of file diff --git a/tests/validators/test_argument_validator.py b/tests/validators/test_argument_validator.py index ca580b0..61b89f4 100644 --- a/tests/validators/test_argument_validator.py +++ b/tests/validators/test_argument_validator.py @@ -17,8 +17,6 @@ Bundle ) -from importspy.config import Config - class TestArgumentValidator: validator = VariableValidator() @@ -54,10 +52,10 @@ def data_4(self) -> Argument: annotation="int", value=10 )] - + @pytest.fixture - def contract_violation(self): - return VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.MODULE_CONTEXT) + def contract_violation(self, functionbundle:Bundle) -> VariableContractViolation: + return VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.MODULE_CONTEXT, functionbundle) @pytest.fixture def argument_value_setter(self, data_3: List[Argument]): @@ -68,23 +66,30 @@ def argument_annotation_setter(self, data_3: List[Argument]): data_3[0].annotation = "str" def test_argument_match(self, data_1: List[Argument], data_4: List[Argument], contract_violation): - assert self.validator.validate(data_1, data_4, contract_violation) + assert data_1 + assert data_4 + assert self.validator.validate(data_1, data_4, contract_violation) is None @pytest.mark.usefixtures("argument_value_setter") def test_argument_match_1(self, data_1: List[Argument], data_3: List[Argument], contract_violation): - assert self.validator.validate(data_1, data_3, contract_violation) + assert data_1 + assert data_3 + assert self.validator.validate(data_1, data_3, contract_violation) is None - def test_argument_mismatch(self, data_2: List[Argument], contract_violation): + def test_argument_mismatch_no_data(self, data_2: List[Argument], contract_violation): assert self.validator.validate(None, data_2, contract_violation) is None - def test_argument_mismatch_1(self, data_3: List[Argument], data_4: List[Argument], contract_violation): + def test_argument_mismatch_1(self, data_3: List[Argument], data_4: List[Argument], contract_violation: VariableContractViolation): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_ARGUMENT_NAME] = "arg1" + mock_bundle[Errors.KEY_FUNCTION_NAME] = "test_function" + mock_contract_violation = VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.MODULE_CONTEXT, mock_bundle) with pytest.raises(ValueError, match=re.escape( - Errors.ARGUMENT_MISMATCH.format( - Constants.VALUE, - data_4[0].name, + mock_contract_violation.mismatch_error_handler( data_4[0].value, - data_3[0].value + data_3[0].value, + Errors.ENTITY_MESSAGES, ) )): self.validator.validate(data_4, data_3, contract_violation) @@ -98,29 +103,45 @@ def test_argument_mismatch_3(self, data_2: List[Argument], contract_violation): @pytest.mark.usefixtures("argument_value_setter") @pytest.mark.usefixtures("argument_annotation_setter") def test_argument_mismatch_5(self, data_1: List[Argument], data_3: List[Argument], contract_violation): - arg_1 = data_3[0] - arg_2 = data_1[0] - with pytest.raises( - ValueError, - match=re.escape(Errors.ARGUMENT_MISMATCH.format(Constants.ANNOTATION, arg_1.name, arg_1.annotation, arg_2.annotation)) - ): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_ARGUMENT_NAME] = "arg1" + mock_bundle[Errors.KEY_FUNCTION_NAME] = "test_function" + mock_contract_violation = VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.MODULE_CONTEXT, mock_bundle) + with pytest.raises(ValueError, + match=re.escape( + mock_contract_violation.mismatch_error_handler( + data_3[0].annotation, + data_1[0].annotation, + Errors.ENTITY_MESSAGES, + ) + )): self.validator.validate(data_3, data_1, contract_violation) def test_argument_mismatch_6(self, data_1: List[Argument], data_3: List[Argument], contract_violation): - arg_1 = data_3[0] - arg_2 = data_1[0] - with pytest.raises( - ValueError, - match=re.escape(Errors.ARGUMENT_MISMATCH.format(Constants.VALUE, arg_1.name, arg_1.value, arg_2.value)) - ): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_ARGUMENT_NAME] = "arg1" + mock_bundle[Errors.KEY_FUNCTION_NAME] = "test_function" + mock_contract_violation = VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.MODULE_CONTEXT, mock_bundle) + with pytest.raises(ValueError, + match=re.escape( + mock_contract_violation.mismatch_error_handler( + data_3[0].value, + data_1[0].value, + Errors.ENTITY_MESSAGES, + ) + )): self.validator.validate(data_3, data_1, contract_violation) def test_argument_mismatch_7(self, data_1: List[Argument], contract_violation): - with pytest.raises( - ValueError, - match=re.escape( - Errors.ELEMENT_MISSING.format(data_1) - ) - ): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_ARGUMENTS_1] = data_1 + mock_bundle[Errors.KEY_FUNCTION_NAME] = "test_function" + mock_contract_violation = VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.MODULE_CONTEXT, mock_bundle) + with pytest.raises(ValueError, + match=re.escape( + mock_contract_violation.missing_error_handler( + Errors.COLLECTIONS_MESSAGES, + ) + )): self.validator.validate(data_1, None, contract_violation) From 7194306436630737efb93ff5c9a8f6429673d85d Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Mon, 2 Jun 2025 17:21:06 +0200 Subject: [PATCH 19/40] refactor(validators): standardize use of VariableContractViolation in test suite - Refactored all validator tests to consistently use VariableContractViolation objects - Unified use of dynamic payloads with proper dispatch between ENTITY_MESSAGES and COLLECTIONS_MESSAGES - Replaced hardcoded class/function names with contextualized bundles - Updated SupportedClassAttributeTypes to use explicit Enum members - Cleaned up unused and redundant imports across tests --- src/importspy/constants.py | 4 +- tests/validators/test_argument_validator.py | 1 - tests/validators/test_attribute_validator.py | 73 ++++++++++++-------- tests/validators/test_function_validator.py | 8 ++- tests/validators/test_module_validator.py | 4 +- tests/validators/test_python_validator.py | 4 +- tests/validators/test_runtime_validator.py | 4 +- tests/validators/test_system_validator.py | 4 +- 8 files changed, 59 insertions(+), 43 deletions(-) diff --git a/src/importspy/constants.py b/src/importspy/constants.py index 3dcb627..7d0b07f 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -56,8 +56,8 @@ class SupportedPythonImplementations(str, Enum): class SupportedClassAttributeTypes(str, Enum): - Config.CLASS_TYPE - Config.INSTANCE_TYPE + CLASS = Config.CLASS_TYPE + INSTANCE = Config.INSTANCE_TYPE NAME = "Name" VALUE = "Value" diff --git a/tests/validators/test_argument_validator.py b/tests/validators/test_argument_validator.py index 61b89f4..759396a 100644 --- a/tests/validators/test_argument_validator.py +++ b/tests/validators/test_argument_validator.py @@ -7,7 +7,6 @@ import re from importspy.constants import ( - Constants, Errors, Contexts ) diff --git a/tests/validators/test_attribute_validator.py b/tests/validators/test_attribute_validator.py index 82c4cf0..836f06d 100644 --- a/tests/validators/test_attribute_validator.py +++ b/tests/validators/test_attribute_validator.py @@ -71,43 +71,45 @@ def instance_type_bundle(self, classbundle) -> Bundle: @pytest.fixture def class_type_contract(self, class_type_bundle): - return VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.CLASS_CONTEXT, class_type_bundle) + return VariableContractViolation(Errors.SCOPE_VARIABLE, Contexts.CLASS_CONTEXT, class_type_bundle) @pytest.fixture def instance_type_contract(self, instance_type_bundle): - return VariableContractViolation(Errors.SCOPE_ARGUMENT, Contexts.CLASS_CONTEXT, instance_type_bundle) + return VariableContractViolation(Errors.SCOPE_VARIABLE, Contexts.CLASS_CONTEXT, instance_type_bundle) @pytest.fixture def attribute_value_setter(self, data_3:Attribute): data_3[0].value = "value" - def test_attribute_match(self, data_1:List[Attribute], data_4:List[Attribute]): - assert self.validator.validate(data_1, data_4, "classname") + def test_attribute_match(self, data_1:List[Attribute], data_4:List[Attribute], class_type_contract): + assert self.validator.validate(data_1, data_4, class_type_contract) is None @pytest.mark.usefixtures("attribute_value_setter") - def test_attribute_match_1(self, data_2:List[Attribute], data_3:List[Attribute]): - assert self.validator.validate(data_2, data_3, "classname") + def test_attribute_match_1(self, data_2:List[Attribute], data_3:List[Attribute], instance_type_contract): + assert self.validator.validate(data_2, data_3, instance_type_contract) is None - def test_attribute_mismatch(self, data_2:List[Attribute]): - assert self.validator.validate(None, data_2, "classname") is None + def test_attribute_mismatch(self, data_2:List[Attribute], class_type_contract): + assert self.validator.validate(None, data_2, class_type_contract) is None - def test_attribute_mismatch_1(self, data_3, data_4): + def test_attribute_mismatch_1(self, data_3, data_4, class_type_contract): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_ATTRIBUTE_NAME] = "class_attribute" + mock_bundle[Errors.KEY_ATTRIBUTE_TYPE] = "class" + mock_bundle[Errors.KEY_CLASS_NAME] = "TestClass" + mock_contract_violation = VariableContractViolation(Errors.SCOPE_VARIABLE, Contexts.CLASS_CONTEXT, mock_bundle) with pytest.raises(ValueError, match=re.escape( - Errors.CLASS_ATTRIBUTE_MISSING.format( - Config.CLASS_TYPE, - f"{data_4[0].name}={data_4[0].value}", - "classname" - ) - )): - self.validator.validate(data_4, data_3, "classname") + mock_contract_violation.missing_error_handler( + Errors.ENTITY_MESSAGES + ))): + self.validator.validate(data_4, data_3, class_type_contract) - def test_attribute_mismatch_2(self): - assert self.validator.validate(None, None, "classname") is None + def test_attribute_mismatch_2(self, instance_type_contract): + assert self.validator.validate(None, None, instance_type_contract) is None - def test_attribute_mismatch_3(self, data_2:List[Attribute]): - assert self.validator.validate(None, data_2, "classname") is None + def test_attribute_mismatch_3(self, data_2:List[Attribute], instance_type_contract): + assert self.validator.validate(None, data_2, instance_type_contract) is None @pytest.fixture def attribute_annotation_setter(self, data_3:Attribute): @@ -115,23 +117,34 @@ def attribute_annotation_setter(self, data_3:Attribute): @pytest.mark.usefixtures("attribute_value_setter") @pytest.mark.usefixtures("attribute_annotation_setter") - def test_attribute_match_3(self, data_2:List[Attribute], data_3:List[Attribute]): - assert self.validator.validate(data_2, data_3, "classname") + def test_attribute_match_3(self, data_2:List[Attribute], data_3:List[Attribute], instance_type_contract): + assert self.validator.validate(data_2, data_3, instance_type_contract) is None @pytest.mark.usefixtures("attribute_value_setter") @pytest.mark.usefixtures("attribute_annotation_setter") - def test_attribute_mismatch_5(self, data_2:List[Attribute], data_3:List[Attribute]): - attr_1 = data_3[0] - attr_2 = data_2[0] + def test_attribute_mismatch_5(self, data_2:List[Attribute], data_3:List[Attribute], instance_type_contract): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_ATTRIBUTE_NAME] = "instance_attribute" + mock_bundle[Errors.KEY_ATTRIBUTE_TYPE] = "instance" + mock_bundle[Errors.KEY_CLASS_NAME] = "TestClass" + mock_contract_violation = VariableContractViolation(Errors.SCOPE_VARIABLE, Contexts.CLASS_CONTEXT, mock_bundle) with pytest.raises( ValueError, - match=re.escape(Errors.CLASS_ATTRIBUTE_MISMATCH.format(Constants.ANNOTATION, attr_1.type, attr_1.name, attr_1.annotation, attr_2.annotation)) + match=re.escape( + mock_contract_violation.mismatch_error_handler(data_3[0].annotation, data_2[0].annotation, Errors.ENTITY_MESSAGES) + ) ): - self.validator.validate(data_3, data_2, "classname") + self.validator.validate(data_3, data_2, instance_type_contract) - def test_attribute_mismatch_6(self, data_3:List[Attribute]): + def test_attribute_mismatch_6(self, data_3:List[Attribute], instance_type_contract): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_ATTRIBUTES_1] = data_3 + mock_bundle[Errors.KEY_CLASS_NAME] = "TestClass" + mock_contract_violation = VariableContractViolation(Errors.SCOPE_VARIABLE, Contexts.CLASS_CONTEXT, mock_bundle) with pytest.raises( ValueError, - match=re.escape(Errors.ELEMENT_MISSING.format(data_3)) + match=re.escape( + mock_contract_violation.missing_error_handler(Errors.COLLECTIONS_MESSAGES) + ) ): - self.validator.validate(data_3, None, "classname") \ No newline at end of file + self.validator.validate(data_3, None, instance_type_contract) \ No newline at end of file diff --git a/tests/validators/test_function_validator.py b/tests/validators/test_function_validator.py index 77f7180..5ac0ddb 100644 --- a/tests/validators/test_function_validator.py +++ b/tests/validators/test_function_validator.py @@ -3,8 +3,8 @@ Function ) from typing import List -from importspy.validators.function_validator import FunctionValidator -from importspy.errors import Errors +from importspy.validators import FunctionValidator +from importspy.constants import Errors import re class TestFunctionValidator: @@ -38,6 +38,10 @@ def data_4(self): return_annotation="str" )] + @pytest.fixture + def class_type_contract(self, class_type_bundle): + return VariableContractViolation(Errors.SCOPE_VARIABLE, Contexts.CLASS_CONTEXT, class_type_bundle) + @pytest.fixture def function_return_annotation_setter(self, data_3:Function): data_3[0].return_annotation = "str" diff --git a/tests/validators/test_module_validator.py b/tests/validators/test_module_validator.py index b35ed04..48d1170 100644 --- a/tests/validators/test_module_validator.py +++ b/tests/validators/test_module_validator.py @@ -3,8 +3,8 @@ Module, Variable ) -from importspy.validators.module_validator import ModuleValidator -from importspy.errors import Errors +from importspy.validators import ModuleValidator +from importspy.constants import Errors import re from typing import List diff --git a/tests/validators/test_python_validator.py b/tests/validators/test_python_validator.py index 410f788..503e3f9 100644 --- a/tests/validators/test_python_validator.py +++ b/tests/validators/test_python_validator.py @@ -3,8 +3,8 @@ Python ) from importspy.config import Config -from importspy.errors import Errors -from importspy.validators.python_validator import PythonValidator +from importspy.constants import Errors +from importspy.validators import PythonValidator from typing import List import re diff --git a/tests/validators/test_runtime_validator.py b/tests/validators/test_runtime_validator.py index 76a1df1..bc4deb5 100644 --- a/tests/validators/test_runtime_validator.py +++ b/tests/validators/test_runtime_validator.py @@ -4,8 +4,8 @@ ) from importspy.config import Config from importspy.constants import Constants -from importspy.validators.runtime_validator import RuntimeValidator -from importspy.errors import Errors +from importspy.validators import RuntimeValidator +from importspy.constants import Errors import re from typing import List diff --git a/tests/validators/test_system_validator.py b/tests/validators/test_system_validator.py index 5098344..25d7595 100644 --- a/tests/validators/test_system_validator.py +++ b/tests/validators/test_system_validator.py @@ -6,8 +6,8 @@ ) from importspy.config import Config from importspy.constants import Constants -from importspy.validators.system_validator import SystemValidator -from importspy.errors import Errors +from importspy.validators import SystemValidator +from importspy.constants import Errors import re from typing import List From 5795a9d0e131c99cdaa4b6abb64520f199a0e80b Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Tue, 3 Jun 2025 21:45:24 +0200 Subject: [PATCH 20/40] refactor(constants, validators): unify dynamic payload keys for function violations - Introduced structured ENTITY_MESSAGES and COLLECTIONS_MESSAGES in FUNCTIONS_DINAMIC_PAYLOAD - Updated FunctionValidator and related tests to use FunctionContractViolation consistently - Replaced hardcoded keys in test fixtures with dynamic resolution based on violation context - Improved test clarity with Bundle mocking and accurate context injection --- src/importspy/constants.py | 17 +++- src/importspy/validators.py | 6 +- tests/conftest.py | 11 ++- tests/validators/test_function_validator.py | 67 +++++++------ tests/validators/test_method_validator.py | 103 ++++++++++++++++++++ 5 files changed, 168 insertions(+), 36 deletions(-) create mode 100644 tests/validators/test_method_validator.py diff --git a/src/importspy/constants.py b/src/importspy/constants.py index 7d0b07f..b4df63a 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -231,6 +231,7 @@ class Category(str, Enum): KEY_ARGUMENTS_1 = "arguments_1" KEY_FUNCTIONS_1 = "functions_1" KEY_CLASSES_1 = "classes_1" + KEY_METHODS_1 = "methods_1" KEY_VARIABLE_NAME = "variable_name" KEY_ARGUMENT_NAME = "argument_name" @@ -284,8 +285,20 @@ class Category(str, Enum): } FUNCTIONS_DINAMIC_PAYLOAD = { - Contexts.MODULE_CONTEXT: KEY_FUNCTION_NAME, - Contexts.CLASS_CONTEXT: KEY_METHOD_NAME + + ENTITY_MESSAGES: { + + Contexts.MODULE_CONTEXT: KEY_FUNCTION_NAME, + Contexts.CLASS_CONTEXT: KEY_METHOD_NAME + + }, + + COLLECTIONS_MESSAGES: { + + Contexts.MODULE_CONTEXT: KEY_FUNCTIONS_1, + Contexts.CLASS_CONTEXT: KEY_METHODS_1 + + } } ERROR_MESSAGE_TEMPLATES = { diff --git a/src/importspy/validators.py b/src/importspy/validators.py index ffc3488..676e36a 100644 --- a/src/importspy/validators.py +++ b/src/importspy/validators.py @@ -458,8 +458,8 @@ def validate( ) ) return - - bundle[Errors.KEY_FUNCTIONS_1] = functions_1 + + bundle[Errors.FUNCTIONS_DINAMIC_PAYLOAD[Errors.COLLECTIONS_MESSAGES][contract_violation.context]] = functions_1 if not functions_2: self.logger.debug( @@ -472,7 +472,7 @@ def validate( raise ValueError(contract_violation.missing_error_handler(Errors.COLLECTIONS_MESSAGES)) for function_1 in functions_1: - bundle[Errors.FUNCTIONS_DINAMIC_PAYLOAD[contract_violation.context]] = function_1.name + bundle[Errors.FUNCTIONS_DINAMIC_PAYLOAD[Errors.ENTITY_MESSAGES][contract_violation.context]] = function_1.name self.logger.debug( Constants.LOG_MESSAGE_TEMPLATE.format( operation="Function validating", diff --git a/tests/conftest.py b/tests/conftest.py index e9f715c..1030f84 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ @pytest.fixture def modulebundle() -> Bundle: bundle = Bundle() - bundle[Errors.KEY_MODULE_NAME] = "testmodule.py" + bundle[Errors.KEY_FILE_NAME] = "testmodule.py" bundle[Errors.KEY_MODULE_VERSION] = "0.1.0" return bundle @@ -21,5 +21,10 @@ def classbundle(modulebundle) -> Bundle: @pytest.fixture def functionbundle(modulebundle) -> Bundle: - modulebundle[Errors.FUNCTIONS_DINAMIC_PAYLOAD[Contexts.MODULE_CONTEXT]] = "test_function" - return modulebundle \ No newline at end of file + modulebundle[Errors.FUNCTIONS_DINAMIC_PAYLOAD[Errors.ENTITY_MESSAGES][Contexts.MODULE_CONTEXT]] = "test_function" + return modulebundle + +@pytest.fixture +def methodbundle(classbundle) -> Bundle: + classbundle[Errors.FUNCTIONS_DINAMIC_PAYLOAD[Errors.ENTITY_MESSAGES][Contexts.CLASS_CONTEXT]] = "test_method" + return classbundle \ No newline at end of file diff --git a/tests/validators/test_function_validator.py b/tests/validators/test_function_validator.py index 5ac0ddb..d8efe5a 100644 --- a/tests/validators/test_function_validator.py +++ b/tests/validators/test_function_validator.py @@ -1,10 +1,18 @@ import pytest -from importspy.models import ( - Function -) +from importspy.models import Function from typing import List from importspy.validators import FunctionValidator -from importspy.constants import Errors + +from importspy.violation_systems import ( + FunctionContractViolation, + Bundle +) + +from importspy.constants import ( + Errors, + Contexts +) + import re class TestFunctionValidator: @@ -39,52 +47,55 @@ def data_4(self): )] @pytest.fixture - def class_type_contract(self, class_type_bundle): - return VariableContractViolation(Errors.SCOPE_VARIABLE, Contexts.CLASS_CONTEXT, class_type_bundle) + def function_contract(self, functionbundle:Bundle): + return FunctionContractViolation(Contexts.MODULE_CONTEXT, functionbundle) @pytest.fixture def function_return_annotation_setter(self, data_3:Function): data_3[0].return_annotation = "str" - def test_function_match(self, data_1:List[Function], data_4:List[Function]): - assert self.validator.validate(data_1, data_4, "classname") + def test_function_match(self, data_1:List[Function], data_4:List[Function], function_contract: FunctionContractViolation): + assert self.validator.validate(data_1, data_4, function_contract) is None - def test_function_mismatch(self, data_2:List[Function]): - assert self.validator.validate(None, data_2, "classname") is None + def test_function_mismatch(self, data_2:List[Function], function_contract): + assert self.validator.validate(None, data_2, function_contract) is None @pytest.mark.usefixtures("function_return_annotation_setter") - def test_function_mismatch_1(self, data_2:List[Function], data_3:List[Function]): + def test_function_mismatch_1(self, data_2:List[Function], data_3:List[Function], function_contract: FunctionContractViolation): with pytest.raises( ValueError, match=re.escape( - Errors.FUNCTIONS_MISSING.format("function", data_2[0].name) + Errors.FUNCTIONS_MISSING.format("function", data_2[0].name, function_contract) ) ): self.validator.validate(data_2, data_3) - def test_function_mismatch_1(self, data_3:List[Function], data_4:List[Function]): + def test_function_mismatch_1(self, data_3:List[Function], data_4:List[Function], function_contract:FunctionContractViolation): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_FILE_NAME] = "testmodule.py" + mock_bundle[Errors.KEY_FUNCTION_NAME] = "function" + mock_contract_violation = FunctionContractViolation(Contexts.MODULE_CONTEXT, mock_bundle) with pytest.raises( ValueError, match=re.escape( - Errors.FUNCTION_RETURN_ANNOTATION_MISMATCH.format( - "method in class classname", - data_4[0].name, - data_4[0].return_annotation, - data_3[0].return_annotation + mock_contract_violation.mismatch_error_handler(data_4[0].return_annotation, data_3[0].return_annotation, Errors.ENTITY_MESSAGES) ) - ) - ): - self.validator.validate(data_4, data_3, "classname") + ): + self.validator.validate(data_4, data_3, function_contract) - def test_function_mismatch_2(self): - assert self.validator.validate(None, None, "classname") is None + def test_function_mismatch_2(self, function_contract:FunctionContractViolation): + assert self.validator.validate(None, None, function_contract) is None - def test_function_mismatch_3(self, data_2:List[Function]): - assert self.validator.validate(None, data_2, "classname") is None + def test_function_mismatch_3(self, data_2:List[Function], function_contract:FunctionContractViolation): + assert self.validator.validate(None, data_2, function_contract) is None - def test_function_mismatch_5(self, data_3:List[Function]): + def test_function_mismatch_5(self, data_3:List[Function], function_contract:FunctionContractViolation): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_FILE_NAME] = "testmodule.py" + mock_bundle[Errors.KEY_FUNCTIONS_1] = data_3 + mock_contract_violation = FunctionContractViolation(Contexts.MODULE_CONTEXT, mock_bundle) with pytest.raises( ValueError, - match=re.escape(Errors.ELEMENT_MISSING.format(data_3)) + match=re.escape(mock_contract_violation.missing_error_handler(Errors.COLLECTIONS_MESSAGES)) ): - self.validator.validate(data_3, None, "classname") \ No newline at end of file + self.validator.validate(data_3, None, function_contract) \ No newline at end of file diff --git a/tests/validators/test_method_validator.py b/tests/validators/test_method_validator.py new file mode 100644 index 0000000..e2b484d --- /dev/null +++ b/tests/validators/test_method_validator.py @@ -0,0 +1,103 @@ +import pytest +from importspy.models import Function +from typing import List +from importspy.validators import FunctionValidator + +from importspy.violation_systems import ( + FunctionContractViolation, + Bundle +) + +from importspy.constants import ( + Errors, + Contexts +) + +import re + +class TestFunctionValidator: + + validator = FunctionValidator() + + @pytest.fixture + def data_1(self): + return [Function( + name="method", + )] + + @pytest.fixture + def data_2(self): + return [Function( + name="getname", + return_annotation="str" + )] + + @pytest.fixture + def data_3(self): + return [Function( + name="method", + return_annotation="int" + )] + + @pytest.fixture + def data_4(self): + return [Function( + name="method", + return_annotation="str" + )] + + @pytest.fixture + def function_contract(self, methodbundle:Bundle): + return FunctionContractViolation(Contexts.CLASS_CONTEXT, methodbundle) + + @pytest.fixture + def function_return_annotation_setter(self, data_3:Function): + data_3[0].return_annotation = "str" + + def test_function_match(self, data_1:List[Function], data_4:List[Function], function_contract: FunctionContractViolation): + assert self.validator.validate(data_1, data_4, function_contract) is None + + def test_function_mismatch(self, data_2:List[Function], function_contract): + assert self.validator.validate(None, data_2, function_contract) is None + + @pytest.mark.usefixtures("function_return_annotation_setter") + def test_function_mismatch_1(self, data_2:List[Function], data_3:List[Function], function_contract: FunctionContractViolation): + with pytest.raises( + ValueError, + match=re.escape( + Errors.FUNCTIONS_MISSING.format("function", data_2[0].name, function_contract) + ) + ): + self.validator.validate(data_2, data_3) + + def test_function_mismatch_1(self, data_3:List[Function], data_4:List[Function], function_contract:FunctionContractViolation): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_FILE_NAME] = "testmodule.py" + mock_bundle[Errors.KEY_CLASS_NAME] = "TestClass" + mock_bundle[Errors.KEY_METHOD_NAME] = "method" + mock_contract_violation = FunctionContractViolation(Contexts.CLASS_CONTEXT, mock_bundle) + with pytest.raises( + ValueError, + match=re.escape( + mock_contract_violation.mismatch_error_handler(data_4[0].return_annotation, data_3[0].return_annotation, Errors.ENTITY_MESSAGES) + ) + ): + self.validator.validate(data_4, data_3, function_contract) + + def test_function_mismatch_2(self, function_contract:FunctionContractViolation): + assert self.validator.validate(None, None, function_contract) is None + + def test_function_mismatch_3(self, data_2:List[Function], function_contract:FunctionContractViolation): + assert self.validator.validate(None, data_2, function_contract) is None + + def test_function_mismatch_5(self, data_3:List[Function], function_contract:FunctionContractViolation): + mock_bundle = Bundle() + mock_bundle[Errors.KEY_FILE_NAME] = "testmodule.py" + mock_bundle[Errors.KEY_CLASS_NAME] = "TestClass" + mock_bundle[Errors.KEY_METHODS_1] = data_3 + mock_contract_violation = FunctionContractViolation(Contexts.CLASS_CONTEXT, mock_bundle) + with pytest.raises( + ValueError, + match=re.escape(mock_contract_violation.missing_error_handler(Errors.COLLECTIONS_MESSAGES)) + ): + self.validator.validate(data_3, None, function_contract) \ No newline at end of file From 399535b96ab4bc04411f5b484677610cfdc26e60 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Tue, 3 Jun 2025 22:45:14 +0200 Subject: [PATCH 21/40] refactor(validators): decouple ModuleValidator from persistent bundle state - ModuleValidator no longer stores a bundle in the constructor - All contract violations are now explicitly passed as arguments - Updated validate method to use context-aware dynamic payload injection - Adjusted tests to reflect contract-based design and error formatting --- src/importspy/validators.py | 26 +++++---- src/importspy/violation_systems.py | 1 - tests/validators/test_attribute_validator.py | 1 - tests/validators/test_module_validator.py | 60 +++++++++++--------- 4 files changed, 48 insertions(+), 40 deletions(-) diff --git a/src/importspy/validators.py b/src/importspy/validators.py index 676e36a..6ad1167 100644 --- a/src/importspy/validators.py +++ b/src/importspy/validators.py @@ -209,8 +209,7 @@ def _is_python_match( class ModuleValidator: - def __init__(self, bundle:Bundle): - self.bundle = bundle + def __init__(self): self.variable_validator:VariableValidator = VariableValidator() self.function_validator:FunctionValidator = FunctionValidator() self.class_validator:ClassValidator = ClassValidator() @@ -218,37 +217,40 @@ def __init__(self, bundle:Bundle): def validate( self, modules_1: List[Module], - module_2: Module + module_2: Module, + contract_violation: ModuleContractViolation + ): + bundle: Bundle = contract_violation.bundle if not modules_1: return - self.bundle[Errors.KEY_MODULES_1] = modules_1 + bundle[Errors.KEY_MODULES_1] = modules_1 if not module_2: raise ValueError( ModuleContractViolation( Contexts.RUNTIME_CONTEXT, - self.bundle + bundle ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) for module_1 in modules_1: - self.bundle[Errors.KEY_MODULE_NAME] = module_1.filename - self.bundle[Errors.KEY_MODULE_VERSION] = module_1.version + bundle[Errors.KEY_MODULE_NAME] = module_1.filename + bundle[Errors.KEY_MODULE_VERSION] = module_1.version if module_1.filename and module_1.filename != module_2.filename: raise ValueError( ModuleContractViolation( Contexts.RUNTIME_CONTEXT, - self.bundle + bundle ).mismatch_error_handler(module_1.filename, module_2.filename, Errors.ENTITY_MESSAGES)) if module_1.version and module_1.version != module_2.version: raise ValueError( ModuleContractViolation( Contexts.RUNTIME_CONTEXT, - self.bundle + bundle ).mismatch_error_handler(module_1.version, module_2.version, Errors.ENTITY_MESSAGES)) self.variable_validator.validate( @@ -257,7 +259,7 @@ def validate( VariableContractViolation( Errors.SCOPE_VARIABLE, Contexts.MODULE_CONTEXT, - self.bundle + bundle ) ) @@ -266,7 +268,7 @@ def validate( module_2.functions, FunctionContractViolation( Contexts.MODULE_CONTEXT, - self.bundle + bundle ) ) @@ -275,7 +277,7 @@ def validate( module_2.classes, ModuleContractViolation( Contexts.CLASS_CONTEXT, - self.bundle + bundle ) ) diff --git a/src/importspy/violation_systems.py b/src/importspy/violation_systems.py index 3d8c3cd..0c187b0 100644 --- a/src/importspy/violation_systems.py +++ b/src/importspy/violation_systems.py @@ -109,7 +109,6 @@ class ModuleContractViolation(BaseContractViolation): def __init__(self, context:str, bundle:'Bundle'): super().__init__(context, bundle) - @property def label(self, spec:str) -> str: return Errors.MODULE_LABEL_TEMPLATE[spec][self.context].format(**self.bundle) diff --git a/tests/validators/test_attribute_validator.py b/tests/validators/test_attribute_validator.py index 836f06d..fa72a94 100644 --- a/tests/validators/test_attribute_validator.py +++ b/tests/validators/test_attribute_validator.py @@ -10,7 +10,6 @@ import re from importspy.constants import ( - Constants, Errors, Contexts ) diff --git a/tests/validators/test_module_validator.py b/tests/validators/test_module_validator.py index 48d1170..8db13dc 100644 --- a/tests/validators/test_module_validator.py +++ b/tests/validators/test_module_validator.py @@ -4,9 +4,14 @@ Variable ) from importspy.validators import ModuleValidator +from importspy.violation_systems import ( + Bundle, + ModuleContractViolation +) from importspy.constants import Errors import re from typing import List +from importspy.constants import Contexts class TestModuleValidator: @@ -24,6 +29,17 @@ def data_2(self): filename="package.py", version="0.1.0" )] + + @pytest.fixture + def data_3(self): + return [Module( + filename="package.py", + version="0.2.0" + )] + + @pytest.fixture + def module_contract(self, methodbundle:Bundle) -> ModuleContractViolation: + return ModuleContractViolation(Contexts.RUNTIME_CONTEXT, methodbundle) @pytest.fixture def filename_setter(self, data_1:List[Module]): @@ -66,43 +82,35 @@ def variables_msg_setter(self, data_3:List[Module]): @pytest.mark.usefixtures("filename_setter") @pytest.mark.usefixtures("version_unsetter") - def test_module_match(self, data_1:List[Module], data_2:List[Module]): - assert self.validator.validate(data_1, data_2[0]) is None + def test_module_match(self, data_1:List[Module], data_2:List[Module], module_contract:ModuleContractViolation): + assert self.validator.validate(data_1, data_2[0], module_contract) is None @pytest.mark.usefixtures("filename_setter") @pytest.mark.usefixtures("variables_setter") @pytest.mark.usefixtures("variables_msg_setter") - def test_module_match_1(self, data_2:List[Module], data_3:List[Module]): - assert self.validator.validate(data_2, data_3[0]) is None + def test_module_match_1(self, data_2:List[Module], data_3:List[Module], module_contract:ModuleContractViolation): + assert self.validator.validate(data_2, data_3[0], module_contract) is None - def test_module_match_2(self, data_2:List[Module], data_3:List[Module]): - assert self.validator.validate(data_2, data_3[0]) is None + def test_module_match_2(self, data_2:List[Module], data_3:List[Module], module_contract:ModuleContractViolation): + assert self.validator.validate(data_2, data_3[0], module_contract) is None - def test_module_mismatch(self, data_2:List[Module]): - assert self.validator.validate(None, data_2[0]) is None - - def test_module_mismatch_2(self, data_2:List[Module], data_3:List[Module]): - with pytest.raises( - ValueError, - match=re.escape(Errors.ELEMENT_MISSING.format(data_3[0].variables)) - ): - self.validator.validate(data_3, data_2[0]) + def test_module_mismatch(self, data_2:List[Module], module_contract:ModuleContractViolation): + assert self.validator.validate(None, data_2[0], module_contract) is None @pytest.mark.usefixtures("version_unsetter") - def test_module_mismatch_3(self, data_1:List[Module], data_2:List[Module]): + def test_module_mismatch_1(self, data_1:List[Module], data_2:List[Module], module_contract:ModuleContractViolation): with pytest.raises(ValueError, match=re.escape( - Errors.FILENAME_MISMATCH.format(data_1[0].filename, - data_2[0].filename) - ) + module_contract.mismatch_error_handler(data_1[0].filename, data_2[0].filename, Errors.ENTITY_MESSAGES) + ) ): - self.validator.validate(data_1, data_2[0]) - - def test_module_mismatch_4(self, data_1:List[Module], data_2:List[Module]): + self.validator.validate(data_1, data_2[0], module_contract) + + @pytest.mark.usefixtures("version_unsetter") + def test_module_mismatch_1(self, data_3:List[Module], data_2:List[Module], module_contract:ModuleContractViolation): with pytest.raises(ValueError, match=re.escape( - Errors.FILENAME_MISMATCH.format(data_1[0].filename, - data_2[0].filename) - ) + module_contract.mismatch_error_handler(data_3[0].version, data_2[0].version, Errors.ENTITY_MESSAGES) + ) ): - self.validator.validate(data_1, data_2[0]) \ No newline at end of file + self.validator.validate(data_3, data_2[0], module_contract) \ No newline at end of file From 0673a288061b1706de9bb8c47a8353dab61becda Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Mon, 16 Jun 2025 14:45:47 +0200 Subject: [PATCH 22/40] refactor(runtime): unify contract violation handling across validators - Removed persistent bundle state from Runtime, System, Python validators - Introduced explicit `contract_violation` injection for all runtime validations - Updated label rendering to support formatted runtime messages - Standardized dynamic payload injection in bundle for error formatting - Fixed key mismatch for systems_1 in error constants - Expanded test coverage for mismatch cases using formatted contract violations - Removed unused legacy code paths and ensured full consistency with Bundle API --- src/importspy/constants.py | 54 +++++------- src/importspy/models.py | 2 +- src/importspy/validators.py | 83 ++++++++++-------- src/importspy/violation_systems.py | 7 +- tests/validators/test_python_validator.py | 98 +++++++++++++++------ tests/validators/test_runtime_validator.py | 49 ++++++++--- tests/validators/test_system_validator.py | 99 ++++++++++++++++------ 7 files changed, 253 insertions(+), 139 deletions(-) diff --git a/src/importspy/constants.py b/src/importspy/constants.py index b4df63a..ce3c69c 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -17,42 +17,34 @@ class Constants: class SupportedArchitectures(str, Enum): - Config.ARCH_x86_64 - Config.ARCH_AARCH64 - Config.ARCH_ARM - Config.ARCH_ARM64 - Config.ARCH_I386 - Config.ARCH_PPC64 - Config.ARCH_PPC64LE - Config.ARCH_S390X + ARCH_x86_64 = Config.ARCH_x86_64 + ARCH_AARCH64 = Config.ARCH_AARCH64 + ARCH_ARM = Config.ARCH_ARM + ARCH_ARM64 = Config.ARCH_ARM64 + ARCH_I386 = Config.ARCH_I386 + ARCH_PPC64 = Config.ARCH_PPC64 + ARCH_PPC64LE = Config.ARCH_PPC64LE + ARCH_S390X = Config.ARCH_S390X class SupportedOS(str, Enum): - Config.OS_WINDOWS - Config.OS_LINUX - Config.OS_MACOS - - class SupportedPythonVersions(str, Enum): - - Config.PYTHON_VERSION_3_13 - Config.PYTHON_VERSION_3_12 - Config.PYTHON_VERSION_3_11 - Config.PYTHON_VERSION_3_10 - Config.PYTHON_VERSION_3_9 + OS_WINDOWS = Config.OS_WINDOWS + OS_LINUX = Config.OS_LINUX + OS_MACOS = Config.OS_MACOS class SupportedPythonImplementations(str, Enum): - Config.INTERPRETER_CPYTHON - Config.INTERPRETER_PYPY - Config.INTERPRETER_JYTHON - Config.INTERPRETER_IRON_PYTHON - Config.INTERPRETER_MICROPYTHON - Config.INTERPRETER_BRYTHON - Config.INTERPRETER_PYSTON - Config.INTERPRETER_GRAALPYTHON - Config.INTERPRETER_RUSTPYTHON - Config.INTERPRETER_NUITKA - Config.INTERPRETER_TRANSCRYPT + INTERPRETER_CPYTHON = Config.INTERPRETER_CPYTHON + INTERPRETER_PYPY = Config.INTERPRETER_PYPY + INTERPRETER_JYTHON = Config.INTERPRETER_JYTHON + INTERPRETER_IRON_PYTHON = Config.INTERPRETER_IRON_PYTHON + INTERPRETER_MICROPYTHON = Config.INTERPRETER_MICROPYTHON + INTERPRETER_BRYTHON = Config.INTERPRETER_BRYTHON + INTERPRETER_PYSTON = Config.INTERPRETER_PYSTON + INTERPRETER_GRAALPYTHON = Config.INTERPRETER_GRAALPYTHON + INTERPRETER_RUSTPYTHON = Config.INTERPRETER_RUSTPYTHON + INTERPRETER_NUITKA = Config.INTERPRETER_NUITKA + INTERPRETER_TRANSCRYPT = Config.INTERPRETER_TRANSCRYPT class SupportedClassAttributeTypes(str, Enum): @@ -220,7 +212,7 @@ class Category(str, Enum): } KEY_RUNTIMES_1 = "runtimes_1" - KEY_SYSTEMS_1 = "runtimes_1" + KEY_SYSTEMS_1 = "systems_1" KEY_PYTHONS_1 = "pythons_1" KEY_PYTHON_1 = "python_1" KEY_ENVIRONMENT_1 = "environment_1" diff --git a/src/importspy/models.py b/src/importspy/models.py index af68dd4..17ef86b 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -47,7 +47,7 @@ class Python(BaseModel): Includes the Python version, interpreter type, and the list of loaded modules. Used to validate compatibility between caller and callee environments. """ - version: Optional[Constants.SupportedPythonVersions] = None + version: Optional[str] = None interpreter: Optional[Constants.SupportedPythonImplementations] = None modules: list['Module'] diff --git a/src/importspy/validators.py b/src/importspy/validators.py index 6ad1167..d53a50a 100644 --- a/src/importspy/validators.py +++ b/src/importspy/validators.py @@ -33,24 +33,24 @@ class RuntimeValidator: - def __init__(self, bundle:Bundle): - self.bundle = bundle - def validate( self, runtimes_1: List[Runtime], - runtimes_2: List[Runtime] + runtimes_2: List[Runtime], + contract_violation: RuntimeContractViolation + ): if not runtimes_1: return - self.bundle[Errors.KEY_RUNTIMES_1] = runtimes_1 + bundle: Bundle = contract_violation.bundle + bundle[Errors.KEY_RUNTIMES_1] = runtimes_1 if not runtimes_2: raise ValueError( RuntimeContractViolation( Contexts.RUNTIME_CONTEXT, - self.bundle + bundle ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) runtime_2 = runtimes_2[0] @@ -60,34 +60,35 @@ def validate( return runtime_1 raise ValueError(RuntimeContractViolation( Contexts.RUNTIME_CONTEXT, - self.bundle + bundle ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) class SystemValidator: - def __init__(self, bundle:Bundle): + def __init__(self): - self.bundle = bundle self._environment_validator = SystemValidator.EnvironmentValidator() def validate( self, systems_1: List[System], - systems_2: List[System] - ) -> None: + systems_2: List[System], + contract_violation: SystemContractViolation + ): if not systems_1: return - self.bundle[Errors.KEY_SYSTEMS_1] = systems_1 + bundle: Bundle = contract_violation.bundle + bundle[Errors.KEY_SYSTEMS_1] = systems_1 if not systems_2: raise ValueError( SystemContractViolation( Contexts.RUNTIME_CONTEXT, - self.bundle + bundle ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) system_2 = systems_2[0] @@ -95,75 +96,88 @@ def validate( for system_1 in systems_1: if system_1.os == system_2.os: if system_1.environment: - self._environment_validator.validate(system_1.environment, system_2.environment) + self._environment_validator.validate(system_1.environment, system_2.environment, bundle) return system_1.pythons raise ValueError( SystemContractViolation( Contexts.RUNTIME_CONTEXT, - self.bundle + bundle ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) class EnvironmentValidator: - - def __init__(self, bundle:Bundle): - self.bundle = bundle def validate(self, environment_1: Environment, - environment_2: Environment + environment_2: Environment, + bundle: Bundle ): if not environment_1: return - self.bundle[Errors.KEY_ENVIRONMENT_1] = environment_1 + bundle[Errors.KEY_ENVIRONMENT_1] = environment_1 if not environment_2: raise ValueError( VariableContractViolation( + Errors.SCOPE_VARIABLE, Contexts.ENVIRONMENT_CONTEXT, - self.bundle + bundle ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) variables_2 = environment_2.variables if environment_1.variables: variables_1 = environment_1.variables - VariableValidator().validate(variables_1, variables_2) + VariableValidator().validate( + variables_1, + variables_2, + VariableContractViolation( + Errors.SCOPE_VARIABLE, + Contexts.ENVIRONMENT_CONTEXT, + bundle + ) + ) return class PythonValidator: - def __init__(self, bundle:Bundle): - self.bundle = bundle - def validate( self, pythons_1: List[Python], - pythons_2: List[Python] + pythons_2: List[Python], + contract_violation: PythonContractViolation ): if not pythons_1: return - self.bundle[Errors.KEY_PYTHONS_1] = pythons_1 + bundle: Bundle = contract_violation.bundle + bundle[Errors.KEY_PYTHONS_1] = pythons_1 if not pythons_2: raise ValueError( PythonContractViolation( Contexts.RUNTIME_CONTEXT, - self.bundle + bundle ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) python_2 = pythons_2[0] for python_1 in pythons_1: - if self._is_python_match(python_1, python_2): + if self._is_python_match(python_1, python_2, contract_violation): return python_1.modules + + raise ValueError( + PythonContractViolation( + Contexts.RUNTIME_CONTEXT, + bundle + ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) def _is_python_match( self, python_1: Python, - python_2: Python + python_2: Python, + contract_violation: PythonContractViolation ) -> bool: """ Determine whether two Python configurations match. @@ -188,7 +202,8 @@ def _is_python_match( - If only interpreter is defined: match interpreter. - If none are defined: match anything (default `True`). """ - self.bundle[Errors.KEY_PYTHON_1] = python_1 + bundle: Bundle = contract_violation.bundle + bundle[Errors.KEY_PYTHON_1] = python_1 if python_1.version and python_1.interpreter: return ( python_1.version == python_2.version and @@ -201,12 +216,6 @@ def _is_python_match( if python_1.interpreter: return python_1.interpreter == python_2.interpreter - raise ValueError( - PythonContractViolation( - Contexts.RUNTIME_CONTEXT, - self.bundle - ).missing_error_handler(Errors.ENTITY_MESSAGES)) - class ModuleValidator: def __init__(self): diff --git a/src/importspy/violation_systems.py b/src/importspy/violation_systems.py index 0c187b0..cd338ed 100644 --- a/src/importspy/violation_systems.py +++ b/src/importspy/violation_systems.py @@ -85,7 +85,7 @@ def __init__(self, context:str, bundle:'Bundle'): super().__init__(context, bundle) def label(self, spec:str) -> str: - return Errors.RUNTIME_LABEL_TEMPLATE[spec] + return Errors.RUNTIME_LABEL_TEMPLATE[spec].format(**self.bundle) class SystemContractViolation(BaseContractViolation): @@ -93,16 +93,15 @@ def __init__(self, context:str, bundle:'Bundle'): super().__init__(context, bundle) def label(self, spec:str) -> str: - return Errors.SYSTEM_LABEL_TEMPLATE[spec] + return Errors.SYSTEM_LABEL_TEMPLATE[spec].format(**self.bundle) class PythonContractViolation(BaseContractViolation): def __init__(self, context:str, bundle:'Bundle'): super().__init__(context, bundle) - @property def label(self, spec:str) -> str: - return Errors.PYTHON_LABEL_TEMPLATE[spec] + return Errors.PYTHON_LABEL_TEMPLATE[spec].format(**self.bundle) class ModuleContractViolation(BaseContractViolation): diff --git a/tests/validators/test_python_validator.py b/tests/validators/test_python_validator.py index 503e3f9..8184f1c 100644 --- a/tests/validators/test_python_validator.py +++ b/tests/validators/test_python_validator.py @@ -3,11 +3,20 @@ Python ) from importspy.config import Config -from importspy.constants import Errors +from importspy.constants import ( + Errors, + Contexts +) + from importspy.validators import PythonValidator from typing import List import re +from importspy.violation_systems import ( + PythonContractViolation, + Bundle +) + class TestPythonValidator: @@ -40,6 +49,20 @@ def data_4(self): modules=[] )] + @pytest.fixture + def pythonbundle(self) -> Bundle: + bundle = Bundle() + bundle[Errors.KEY_PYTHON_1] = Python( + version=Config.PYTHON_VERSION_3_13, + interpreter=Config.INTERPRETER_IRON_PYTHON, + modules=[] + ) + return bundle + + @pytest.fixture + def python_contract(self, pythonbundle: Bundle) -> PythonContractViolation: + return PythonContractViolation(context=Contexts.RUNTIME_CONTEXT, bundle=pythonbundle) + @pytest.fixture def python_version_setter(self, data_2:List[Python]): data_2[0].version = "12.0.1" @@ -48,34 +71,61 @@ def python_version_setter(self, data_2:List[Python]): def python_interpreter_setter(self, data_3:List[Python]): data_3[0].interpreter = Config.INTERPRETER_GRAALPYTHON - @pytest.mark.usefixtures("python_version_setter") - def test_python_match(self, data_1:List[Python], data_2:List[Python]): - assert self.validator.validate(data_1, data_2) is None - - def test_python_match_1(self, data_3:List[Python], data_4:List[Python]): - assert self.validator.validate(data_3, data_4) is None + def test_python_match(self, data_3:List[Python], data_4:List[Python], python_contract): + assert self.validator.validate(data_3, data_4, python_contract) == data_3[0].modules - def test_python_mismatch(self, data_2:List[Python]): - assert self.validator.validate(None, data_2) is None + def test_python_mismatch(self, data_2:List[Python], python_contract): + assert self.validator.validate(None, data_2, python_contract) is None @pytest.mark.usefixtures("python_interpreter_setter") - def test_python_mismatch_1(self, data_3, data_4): - assert self.validator.validate(data_4, data_3) is None + def test_python_mismatch_1(self, data_3, data_4, python_contract): + mock_contract = PythonContractViolation( + Contexts.RUNTIME_CONTEXT, + Bundle( + state= { Errors.KEY_PYTHONS_1 : data_4 } + ) + ) + with pytest.raises(ValueError, + match=re.escape( + mock_contract.missing_error_handler(Errors.COLLECTIONS_MESSAGES) + ) + ): + assert self.validator.validate(data_4, data_3, python_contract) + + def test_python_mismatch_2(self, python_contract): + assert self.validator.validate(None, None, python_contract) is None - def test_python_mismatch_2(self): - assert self.validator.validate(None, None) is None + def test_python_mismatch_3(self, python_contract): + assert self.validator.validate(None, None, python_contract) is None - def test_python_mismatch_3(self): - assert self.validator.validate(None, None) is None + def test_python_mismatch_4(self, data_2:List[Python], python_contract): + assert self.validator.validate(None, data_2, python_contract) is None - def test_python_mismatch_4(self, data_2:List[Python]): - assert self.validator.validate(None, data_2) is None + def test_python_mismatch_5(self, data_3:List[Python], python_contract): + mock_contract = PythonContractViolation( + Contexts.RUNTIME_CONTEXT, + Bundle( + state= { Errors.KEY_PYTHONS_1 : data_3 } + ) + ) + with pytest.raises(ValueError, + match=re.escape( + mock_contract.missing_error_handler(Errors.COLLECTIONS_MESSAGES) + ) + ): + self.validator.validate(data_3, None, python_contract) - def test_python_mismatch_5(self, data_3:List[Python]): - with pytest.raises( - ValueError, - match=re.escape( - Errors.ELEMENT_MISSING.format(data_3) - ) + @pytest.mark.usefixtures("python_version_setter") + def test_python_mismatch_6(self, data_1:List[Python], data_2:List[Python], python_contract:PythonContractViolation): + mock_contract = PythonContractViolation( + Contexts.RUNTIME_CONTEXT, + Bundle( + state= { Errors.KEY_PYTHONS_1 : data_1 } + ) + ) + with pytest.raises(ValueError, + match=re.escape( + mock_contract.missing_error_handler(Errors.COLLECTIONS_MESSAGES) + ) ): - self.validator.validate(data_3, None) \ No newline at end of file + self.validator.validate(data_1, data_2, python_contract) \ No newline at end of file diff --git a/tests/validators/test_runtime_validator.py b/tests/validators/test_runtime_validator.py index bc4deb5..ab647c9 100644 --- a/tests/validators/test_runtime_validator.py +++ b/tests/validators/test_runtime_validator.py @@ -3,11 +3,20 @@ Runtime ) from importspy.config import Config -from importspy.constants import Constants + from importspy.validators import RuntimeValidator -from importspy.constants import Errors +from importspy.constants import ( + Errors, + Constants, + Contexts +) + import re from typing import List +from importspy.violation_systems import ( + Bundle, + RuntimeContractViolation +) class TestRuntimeValidator: @@ -27,21 +36,33 @@ def data_2(self): systems=[] )] + @pytest.fixture + def runtimebundle(self) -> Bundle: + bundle = Bundle() + return bundle + + @pytest.fixture + def runtime_contract(self, runtimebundle: Bundle) -> RuntimeContractViolation: + return RuntimeContractViolation(context=Contexts.RUNTIME_CONTEXT, bundle=runtimebundle) + @pytest.fixture def arch_arm_setter(self, data_1:List[Runtime]): data_1[0].arch = Config.ARCH_ARM - def test_runtime_arch_match(self, data_1:List[Runtime], data_2:List[Runtime]): - assert self.validator.validate(data_1, data_2) is None - - def test_runtime_arch_invalid(self): - with pytest.raises(ValueError, - match=re.escape(Errors.INVALID_ARCHITECTURE.format("A invalid value", Constants.KNOWN_ARCHITECTURES))): - Runtime( - arch="A invalid value", - systems=[] - ) + def test_runtime_arch_match(self, data_1:List[Runtime], data_2:List[Runtime], runtime_contract): + assert self.validator.validate(data_1, data_2, runtime_contract) == data_1[0] @pytest.mark.usefixtures("arch_arm_setter") - def test_runtime_arch_mismatch(self, data_1:List[Runtime], data_2:List[Runtime]): - assert self.validator.validate(data_1, data_2) is None \ No newline at end of file + def test_runtime_arch_mismatch(self, data_1:List[Runtime], data_2:List[Runtime], runtime_contract): + mock_contract = RuntimeContractViolation( + Contexts.RUNTIME_CONTEXT, + Bundle( + state= { Errors.KEY_RUNTIMES_1 : data_1 } + ) + ) + with pytest.raises(ValueError, + match=re.escape( + mock_contract.missing_error_handler(Errors.COLLECTIONS_MESSAGES) + ) + ): + assert self.validator.validate(data_1, data_2, runtime_contract) is None \ No newline at end of file diff --git a/tests/validators/test_system_validator.py b/tests/validators/test_system_validator.py index 25d7595..1a197c2 100644 --- a/tests/validators/test_system_validator.py +++ b/tests/validators/test_system_validator.py @@ -5,12 +5,23 @@ Variable ) from importspy.config import Config -from importspy.constants import Constants + +from importspy.constants import ( + Constants, + Contexts +) + from importspy.validators import SystemValidator from importspy.constants import Errors import re from typing import List +from importspy.violation_systems import ( + Bundle, + SystemContractViolation, + VariableContractViolation +) + class TestSystemValidator: validator = SystemValidator() @@ -35,6 +46,19 @@ def data_2(self): pythons=[] )] + @pytest.fixture + def systembundle(self) -> Bundle: + bundle = Bundle() + return bundle + + @pytest.fixture + def variable_contract(self, systembundle) -> VariableContractViolation: + return VariableContractViolation(Errors.SCOPE_VARIABLE, Contexts.ENVIRONMENT_CONTEXT, systembundle) + + @pytest.fixture + def system_contract(self, systembundle) -> SystemContractViolation: + return SystemContractViolation(Contexts.RUNTIME_CONTEXT, systembundle) + @pytest.fixture def os_windows_setter(self, data_1:List[System]): data_1[0].os = Config.OS_WINDOWS @@ -44,39 +68,58 @@ def envs_setter(self, data_2): data_2[0].environment = Environment(variables=[(Variable(name="CI", value="true"))]) @pytest.mark.usefixtures("envs_setter") - def test_system_os_match(self, data_1:List[System], data_2:List[System]): - assert self.validator.validate(data_1, data_2) is None + def test_system_os_match(self, data_1:List[System], data_2:List[System], system_contract: SystemContractViolation): + assert self.validator.validate(data_1, data_2, system_contract) == data_1[0].pythons - def test_system_os_match_2(self, data_1:List[System], data_2:List[System]): - assert self.validator.validate(data_2, data_1) is None - - def test_system_os_invalid(self): - with pytest.raises(ValueError, - match=re.escape(Errors.INVALID_OS.format(Constants.SUPPORTED_OS, "A invalid value"))): - System( - os="A invalid value", - pythons=[] - ) + def test_system_os_match_2(self, data_1:List[System], data_2:List[System], system_contract: SystemContractViolation): + assert self.validator.validate(data_2, data_1, system_contract) == data_2[0].pythons - def test_system_os_mismatch(self, data_1:List[System], data_2:List[System]): - with pytest.raises( - ValueError, - match=re.escape( - Errors.ELEMENT_MISSING.format(data_1[0].environment) + def test_system_os_mismatch(self, data_1:List[System], data_2:List[System], system_contract: SystemContractViolation): + mock_contract = VariableContractViolation( + Errors.SCOPE_VARIABLE, + Contexts.ENVIRONMENT_CONTEXT, + Bundle( + state= { + Errors.KEY_ENVIRONMENT_1 : data_1[0].environment, + Errors.KEY_ENVIRONMENT_VARIABLE_NAME: "CI" + } ) + ) + with pytest.raises(ValueError, + match=re.escape( + mock_contract.missing_error_handler(Errors.COLLECTIONS_MESSAGES) + ) ): - self.validator.validate(data_1, data_2) + self.validator.validate(data_1, data_2, system_contract) @pytest.mark.usefixtures("os_windows_setter") - def test_system_os_mismatch_1(self, data_1:List[System], data_2:List[System]): - assert self.validator.validate(data_1, data_2) is None + def test_system_os_mismatch_1(self, data_1:List[System], data_2:List[System], system_contract: SystemContractViolation): + bundle: Bundle = Bundle() + bundle[Errors.KEY_SYSTEMS_1] = data_1 + mock_contract = SystemContractViolation( + Contexts.RUNTIME_CONTEXT, + bundle + ) + with pytest.raises(ValueError, + match=re.escape( + mock_contract.missing_error_handler(Errors.COLLECTIONS_MESSAGES) + ) + ): + self.validator.validate(data_1, data_2, system_contract) - def test_system_mismatch(self, data_2:List[System]): - assert self.validator.validate(None, data_2) is None + def test_system_mismatch(self, data_2:List[System], system_contract: SystemContractViolation): + assert self.validator.validate(None, data_2, system_contract) is None - def test_system_mismatch_1(self, data_2:List[System]): - with pytest.raises( - ValueError, - match=re.escape(Errors.ELEMENT_MISSING.format(data_2)) + def test_system_mismatch_1(self, data_2:List[System], system_contract: SystemContractViolation): + bundle: Bundle = Bundle() + bundle[Errors.KEY_SYSTEMS_1] = data_2 + mock_contract = SystemContractViolation( + Contexts.RUNTIME_CONTEXT, + bundle + ) + with pytest.raises(ValueError, + match=re.escape( + mock_contract.missing_error_handler(Errors.COLLECTIONS_MESSAGES) + ) ): - self.validator.validate(data_2, None) \ No newline at end of file + self.validator.validate(data_2, None, system_contract) \ No newline at end of file From 93d11cefb74b82c71505b926e2335c38e26d6e50 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Tue, 17 Jun 2025 16:08:28 +0200 Subject: [PATCH 23/40] refactor: improve error reporting and runtime validation formatting - Added __str__ and __repr__ methods to core model classes (Python, System, Variable, Module) for cleaner error outputs - Refactored error message templates to support entity vs. collection granularity - Corrected YAML model definition for superclass (now supports name field) - Improved runtime validation pipeline to use context-aware contract violations - Replaced DEBUG with WARN for external compliance example logging - Removed unused plugin_interface.py example module - Adjusted environment variable extraction to include annotation field - Fixed incorrect error key (systems_1) and enhanced template resolution in BaseContractViolation --- .../external_module_compilance/package.py | 2 +- .../external_module_compilance/spymodel.yml | 6 +-- .../pipeline_validation/plugin_interface.py | 2 - .../pipeline_validation/spymodel.yml | 4 +- src/importspy/constants.py | 35 +++++++++++++---- src/importspy/models.py | 38 +++++++++++++++++-- src/importspy/s.py | 25 ++++++++---- src/importspy/utilities/python_util.py | 7 +--- src/importspy/utilities/system_util.py | 4 +- src/importspy/validators.py | 7 ---- src/importspy/violation_systems.py | 6 +-- 11 files changed, 91 insertions(+), 45 deletions(-) delete mode 100644 examples/plugin_based_architecture/pipeline_validation/plugin_interface.py diff --git a/examples/plugin_based_architecture/external_module_compilance/package.py b/examples/plugin_based_architecture/external_module_compilance/package.py index b1a6a70..14474df 100644 --- a/examples/plugin_based_architecture/external_module_compilance/package.py +++ b/examples/plugin_based_architecture/external_module_compilance/package.py @@ -6,5 +6,5 @@ __version__ = None -caller_module = Spy().importspy(filepath="./spymodel.yml", log_level=logging.DEBUG) +caller_module = Spy().importspy(filepath="./spymodel.yml", log_level=logging.WARN) caller_module.Foo().get_bar() \ No newline at end of file diff --git a/examples/plugin_based_architecture/external_module_compilance/spymodel.yml b/examples/plugin_based_architecture/external_module_compilance/spymodel.yml index a3cb127..0dc611f 100644 --- a/examples/plugin_based_architecture/external_module_compilance/spymodel.yml +++ b/examples/plugin_based_architecture/external_module_compilance/spymodel.yml @@ -34,7 +34,7 @@ classes: arguments: - name: self superclasses: - - Plugin + - name: Plugin - name: Foo attributes: methods: @@ -63,9 +63,9 @@ deployments: - interpreter: IronPython modules: - filename: addons.py - - os: linux + - os: windows pythons: - - version: 3.12.8 + - version: 3.12.9 interpreter: CPython modules: - filename: extension.py diff --git a/examples/plugin_based_architecture/pipeline_validation/plugin_interface.py b/examples/plugin_based_architecture/pipeline_validation/plugin_interface.py deleted file mode 100644 index cd3c151..0000000 --- a/examples/plugin_based_architecture/pipeline_validation/plugin_interface.py +++ /dev/null @@ -1,2 +0,0 @@ -class Plugin: - pass \ No newline at end of file diff --git a/examples/plugin_based_architecture/pipeline_validation/spymodel.yml b/examples/plugin_based_architecture/pipeline_validation/spymodel.yml index a3cb127..9be9d9d 100644 --- a/examples/plugin_based_architecture/pipeline_validation/spymodel.yml +++ b/examples/plugin_based_architecture/pipeline_validation/spymodel.yml @@ -34,7 +34,7 @@ classes: arguments: - name: self superclasses: - - Plugin + - name: Plugin - name: Foo attributes: methods: @@ -65,7 +65,7 @@ deployments: - filename: addons.py - os: linux pythons: - - version: 3.12.8 + - version: 3.12.9 interpreter: CPython modules: - filename: extension.py diff --git a/src/importspy/constants.py b/src/importspy/constants.py index ce3c69c..650708b 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -123,7 +123,7 @@ class Category(str, Enum): SYSTEM_LABEL_TEMPLATE = { ENTITY_MESSAGES: 'The system "{system_1}"', - COLLECTIONS_MESSAGES: 'The systems "{systems_1}"' + COLLECTIONS_MESSAGES: 'systems "{systems_1}"' } @@ -295,15 +295,34 @@ class Category(str, Enum): ERROR_MESSAGE_TEMPLATES = { Category.MISSING: { - TEMPLATE_KEY: "{label} is declared but missing.", - SOLUTION_KEY: "Ensure it is properly defined and implemented." + ENTITY_MESSAGES: { + TEMPLATE_KEY: '{label} is declared but missing.', + SOLUTION_KEY: 'Ensure it is properly defined and implemented.' + }, + COLLECTIONS_MESSAGES: { + TEMPLATE_KEY: '{label} are declared but missing.', + SOLUTION_KEY: 'Ensure all of them are properly defined and implemented.' + } }, Category.MISMATCH: { - TEMPLATE_KEY: "{label} does not match the expected value. Expected: {expected!r}, Found: {actual!r}.", - SOLUTION_KEY: "Check the implementation or update the contract accordingly." + ENTITY_MESSAGES: { + TEMPLATE_KEY: '{label} does not match the expected value. Expected: {expected!r}, Found: {actual!r}.', + SOLUTION_KEY: 'Check the value and update the contract or implementation accordingly.' + }, + COLLECTIONS_MESSAGES: { + TEMPLATE_KEY: '{label} do not match the expected values. Expected: {expected!r}, Found: {actual!r}.', + SOLUTION_KEY: 'Review the values and update the contract or implementation as needed.' + } }, Category.INVALID: { - TEMPLATE_KEY: "{label} has an invalid value. Allowed values: {allowed}. Found: {found!r}.", - SOLUTION_KEY: "Update the environment or contract accordingly." + ENTITY_MESSAGES: { + TEMPLATE_KEY: '{label} has an invalid value. Allowed values: {allowed}. Found: {found!r}.', + SOLUTION_KEY: 'Update the value to one of the allowed options.' + }, + COLLECTIONS_MESSAGES: { + TEMPLATE_KEY: '{label} have invalid values. Allowed values: {allowed}. Found: {found!r}.', + SOLUTION_KEY: 'Update the values to be within the allowed options.' + } } - } \ No newline at end of file + } + diff --git a/src/importspy/models.py b/src/importspy/models.py index 17ef86b..d3976e9 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -51,6 +51,12 @@ class Python(BaseModel): interpreter: Optional[Constants.SupportedPythonImplementations] = None modules: list['Module'] + def __str__(self): + return f"{self.interpreter.value} v{self.version}" + + def __repr__(self): + return str(self) + class Environment(BaseModel): """ Represents a set of environment variables and secret keys @@ -68,6 +74,15 @@ class System(BaseModel): environment: Optional[Environment] = None pythons: list[Python] + def __str__(self): + pretty_string = "" + if self.os: + pretty_string = self.os.value + return f"{self.os.value}" + + def __repr__(self): + return str(self) + class Runtime(BaseModel): """ @@ -97,6 +112,13 @@ def from_variable_info(cls, variables_info: list[VariableInfo]): value=var_info.value, annotation=var_info.annotation ) for var_info in variables_info] + + def __str__(self): + type_part = f": {self.annotation}" if self.annotation else "" + return f"{self.name}{type_part} = {self.value}" + + def __repr__(self): + return str(self) class Attribute(Variable): @@ -176,14 +198,16 @@ def from_class_info(cls, extracted_classes: list[ClassInfo]): name=name, attributes=Attribute.from_attributes_info(attributes), methods=Function.from_functions_info(methods), - superclasses=superclasses + superclasses= cls.from_class_info(superclasses) ) for name, attributes, methods, superclasses in extracted_classes] def get_class_attributes(self) -> List[Attribute]: - return [attr for attr in self.attributes if attr.type == Config.CLASS_TYPE] + if self.attributes: + return [attr for attr in self.attributes if attr.type == Config.CLASS_TYPE] def get_instance_attributes(self) -> List[Attribute]: - return [attr for attr in self.attributes if attr.type == Config.INSTANCE_TYPE] + if self.attributes: + return [attr for attr in self.attributes if attr.type == Config.INSTANCE_TYPE] class Module(BaseModel): @@ -197,6 +221,12 @@ class Module(BaseModel): functions: Optional[list[Function]] = None classes: Optional[list[Class]] = None + def __str__(self): + return f"Module: {self.filename or 'unknown'} (v{self.version or '-'})" + + def __repr__(self): + return str(self) + class SpyModel(Module): """ @@ -231,7 +261,7 @@ def from_module(cls, info_module: ModuleType): os = system_utils.extract_os() python_version = python_utils.extract_python_version() interpreter = python_utils.extract_python_implementation() - envs = system_utils.extract_envs() + envs = Variable.from_variable_info(system_utils.extract_envs()) module_utils.unload_module(info_module) logger.debug("Unload module") diff --git a/src/importspy/s.py b/src/importspy/s.py index 1dee4a7..aea802e 100644 --- a/src/importspy/s.py +++ b/src/importspy/s.py @@ -42,8 +42,15 @@ ) import logging -from .violation_systems import Bundle +from .violation_systems import ( + Bundle, + ModuleContractViolation, + RuntimeContractViolation, + SystemContractViolation, + PythonContractViolation +) +from .constants import Contexts class Spy: """ @@ -153,15 +160,19 @@ def _validate_module(self, spymodel: SpyModel, info_module: ModuleType) -> Modul self.logger.debug(f"info_module: {info_module}") if spymodel: bundle = Bundle() - module_validator:ModuleValidator = ModuleValidator(bundle) + module_validator:ModuleValidator = ModuleValidator() self.logger.debug(f"Import contract detected: {spymodel}") spy_module = SpyModel.from_module(info_module) self.logger.debug(f"Extracted module structure: {spy_module}") - module_validator.validate([spymodel],spy_module.deployments[0].systems[0].pythons[0].modules[0]) - runtime:Runtime = RuntimeValidator(bundle).validate(spymodel.deployments, spy_module.deployments) - pythons:List[Python] = SystemValidator(bundle).validate(runtime.systems, spy_module.deployments[0].systems) - modules: List[Module] = PythonValidator(bundle).validate(pythons, spy_module.deployments[0].systems[0].pythons) - module_validator.validate(modules, spy_module.deployments[0].systems[0].pythons[0].modules[0]) + module_contract: ModuleContractViolation = ModuleContractViolation(Contexts.MODULE_CONTEXT, bundle) + module_validator.validate([spymodel],spy_module.deployments[0].systems[0].pythons[0].modules[0], module_contract) + runtime_contract: RuntimeContractViolation = RuntimeContractViolation(Contexts.RUNTIME_CONTEXT, bundle) + runtime:Runtime = RuntimeValidator().validate(spymodel.deployments, spy_module.deployments, runtime_contract) + system_contract: SystemContractViolation = SystemContractViolation(Contexts.RUNTIME_CONTEXT, bundle) + pythons:List[Python] = SystemValidator().validate(runtime.systems, spy_module.deployments[0].systems, system_contract) + python_contract: PythonContractViolation = PythonContractViolation(Contexts.RUNTIME_CONTEXT, bundle) + modules: List[Module] = PythonValidator().validate(pythons, spy_module.deployments[0].systems[0].pythons, python_contract) + module_validator.validate(modules, spy_module.deployments[0].systems[0].pythons[0].modules[0], module_contract) return ModuleUtil().load_module(info_module) def _inspect_module(self) -> ModuleType: diff --git a/src/importspy/utilities/python_util.py b/src/importspy/utilities/python_util.py index 0197025..d8f2b9d 100644 --- a/src/importspy/utilities/python_util.py +++ b/src/importspy/utilities/python_util.py @@ -46,14 +46,9 @@ def extract_python_version(self) -> str: ------- str Python version string (e.g., '3.11.2'). - - Example - ------- - >>> PythonUtil().extract_python_version() - '3.11' """ python_version = platform.python_version() - return ".".join(python_version.split(".")[:2]) + return python_version def extract_python_implementation(self) -> str: """ diff --git a/src/importspy/utilities/system_util.py b/src/importspy/utilities/system_util.py index 415234f..401d428 100644 --- a/src/importspy/utilities/system_util.py +++ b/src/importspy/utilities/system_util.py @@ -32,7 +32,7 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) -VariableInfo = namedtuple('VariableInfo', ["name", "value"]) +VariableInfo = namedtuple('VariableInfo', ["name", "annotation", "value"]) class SystemUtil: """ @@ -84,5 +84,5 @@ def extract_envs(self) -> List[VariableInfo]: >>> SystemUtil().extract_envs() [VariableInfo(name='PATH', value='/usr/bin'), VariableInfo(name='HOME', value='/home/user'), ...] """ - return [VariableInfo(name, value) for name, value in os.environ.items()] + return [VariableInfo(name, None, value) for name, value in os.environ.items()] diff --git a/src/importspy/validators.py b/src/importspy/validators.py index d53a50a..d93f000 100644 --- a/src/importspy/validators.py +++ b/src/importspy/validators.py @@ -138,7 +138,6 @@ def validate(self, bundle ) ) - return class PythonValidator: @@ -355,12 +354,6 @@ def validate( ) ) - self.function_validator.validate( - class_1.methods, - class_2.methods, - classname=class_1.name - ) - self.validate(class_1.superclasses, class_2.superclasses, ModuleContractViolation( diff --git a/src/importspy/violation_systems.py b/src/importspy/violation_systems.py index cd338ed..8d923b4 100644 --- a/src/importspy/violation_systems.py +++ b/src/importspy/violation_systems.py @@ -54,13 +54,13 @@ def context(self) -> str: return self._context def missing_error_handler(self, spec:str) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][Errors.TEMPLATE_KEY].format(label=self.label(spec))} - {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][Errors.SOLUTION_KEY].capitalize()}' + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][spec][Errors.TEMPLATE_KEY].format(label=self.label(spec))} - {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][spec][Errors.SOLUTION_KEY].capitalize()}' def mismatch_error_handler(self, expected:Any, actual:Any, spec:str) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][Errors.TEMPLATE_KEY].format(label=self.label(spec), expected=expected, actual=actual)} - {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][Errors.SOLUTION_KEY].capitalize()}' + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][spec][Errors.TEMPLATE_KEY].format(label=self.label(spec), expected=expected, actual=actual)} - {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][spec][Errors.SOLUTION_KEY].capitalize()}' def invalid_error_handler(self, allowed:Any, found:Any, spec:str) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][Errors.TEMPLATE_KEY].format(label=self.label(spec), expected=allowed, actual=found)} - {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][Errors.SOLUTION_KEY].capitalize()}' + return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][spec][Errors.TEMPLATE_KEY].format(label=self.label(spec), expected=allowed, actual=found)} - {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][spec][Errors.SOLUTION_KEY].capitalize()}' class VariableContractViolation(BaseContractViolation): From ff0816c814deef82b7ead8146df45c4d5a960af3 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Tue, 17 Jun 2025 21:40:02 +0200 Subject: [PATCH 24/40] refactor(models): improve string representations for core model classes - Added __str__ and __repr__ methods to Environment, Runtime, and Function classes - Simplified System.__str__ method by removing unnecessary logic - Adjusted Python.__str__ to avoid accessing .value on plain strings - Updated test fixtures to use SupportedOS enum for clarity and consistency --- src/importspy/models.py | 24 +++++++++++++++++++---- tests/validators/test_system_validator.py | 8 ++++---- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/importspy/models.py b/src/importspy/models.py index d3976e9..2f00145 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -52,7 +52,7 @@ class Python(BaseModel): modules: list['Module'] def __str__(self): - return f"{self.interpreter.value} v{self.version}" + return f"{self.interpreter} v{self.version}" def __repr__(self): return str(self) @@ -65,6 +65,12 @@ class Environment(BaseModel): variables: Optional[list['Variable']] = None secrets: Optional[list[str]] = None + def __str__(self): + return f"variables: {self.variables} | secrets: {self.secrets}" + + def __repr__(self): + return str(self) + class System(BaseModel): """ Represents the system environment, including OS, environment variables, @@ -75,9 +81,6 @@ class System(BaseModel): pythons: list[Python] def __str__(self): - pretty_string = "" - if self.os: - pretty_string = self.os.value return f"{self.os.value}" def __repr__(self): @@ -92,6 +95,12 @@ class Runtime(BaseModel): arch: Constants.SupportedArchitectures systems: list[System] + def __str__(self): + return f"{self.arch}" + + def __repr__(self): + return str(self) + class Variable(BaseModel): """ @@ -177,6 +186,13 @@ def from_functions_info(cls, functions_info: list[FunctionInfo]): arguments=Argument.from_arguments_info(func_info.arguments), return_annotation=func_info.return_annotation ) for func_info in functions_info] + + def __str__(self): + formatted_arguments = f"{', '.join(str(arg) for arg in self.arguments)}" if self.arguments else "" + return f"{self.name}({formatted_arguments}) -> {self.return_annotation}" + + def __repr__(self): + return str(self) class Class(BaseModel): diff --git a/tests/validators/test_system_validator.py b/tests/validators/test_system_validator.py index 1a197c2..010b2e5 100644 --- a/tests/validators/test_system_validator.py +++ b/tests/validators/test_system_validator.py @@ -7,8 +7,8 @@ from importspy.config import Config from importspy.constants import ( - Constants, - Contexts + Contexts, + Constants ) from importspy.validators import SystemValidator @@ -29,7 +29,7 @@ class TestSystemValidator: @pytest.fixture def data_1(self): return [System( - os=Config.OS_LINUX, + os=Constants.SupportedOS.OS_LINUX, environment=Environment( variables=[Variable( name="CI", @@ -42,7 +42,7 @@ def data_1(self): @pytest.fixture def data_2(self): return [System( - os=Config.OS_LINUX, + os=Constants.SupportedOS.OS_LINUX, pythons=[] )] From 41b96c83c4f03558383ea85164d175e36381885c Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Fri, 20 Jun 2025 16:55:43 +0200 Subject: [PATCH 25/40] chore: remove unused import and fix test enum usage - Removed unused `Any` import from models.py - Updated test fixture in `test_system_validator.py` to use SupportedOS enum for setting system OS --- src/importspy/models.py | 1 - tests/validators/test_system_validator.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/importspy/models.py b/src/importspy/models.py index 2f00145..c4f99e3 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -13,7 +13,6 @@ from typing import ( Optional, Union, - Any, List ) diff --git a/tests/validators/test_system_validator.py b/tests/validators/test_system_validator.py index 010b2e5..fa8effa 100644 --- a/tests/validators/test_system_validator.py +++ b/tests/validators/test_system_validator.py @@ -61,7 +61,7 @@ def system_contract(self, systembundle) -> SystemContractViolation: @pytest.fixture def os_windows_setter(self, data_1:List[System]): - data_1[0].os = Config.OS_WINDOWS + data_1[0].os = Constants.SupportedOS.OS_WINDOWS @pytest.fixture def envs_setter(self, data_2): From 3b2e0a037fbad8d0a21462dbd3cf7d7826241f01 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Sun, 6 Jul 2025 18:02:11 +0200 Subject: [PATCH 26/40] chore: whole deps updated --- poetry.lock | 116 +++++++++++++++++++++++++++------------------------- 1 file changed, 60 insertions(+), 56 deletions(-) diff --git a/poetry.lock b/poetry.lock index b74e3f8..4a46ad2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -60,13 +60,13 @@ lxml = ["lxml"] [[package]] name = "certifi" -version = "2025.4.26" +version = "2025.6.15" description = "Python package for providing Mozilla's CA Bundle." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3"}, - {file = "certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6"}, + {file = "certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057"}, + {file = "certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b"}, ] [[package]] @@ -208,15 +208,18 @@ files = [ [[package]] name = "exceptiongroup" -version = "1.2.2" +version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + [package.extras] test = ["pytest (>=6)"] @@ -408,28 +411,28 @@ files = [ [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "pydantic" -version = "2.11.4" +version = "2.11.7" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" files = [ - {file = "pydantic-2.11.4-py3-none-any.whl", hash = "sha256:d9615eaa9ac5a063471da949c8fc16376a84afb5024688b3ff885693506764eb"}, - {file = "pydantic-2.11.4.tar.gz", hash = "sha256:32738d19d63a226a52eed76645a98ee07c1f410ee41d93b4afbfa85ed8111c2d"}, + {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, + {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, ] [package.dependencies] @@ -555,13 +558,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pygments" -version = "2.19.1" +version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, ] [package.extras] @@ -569,40 +572,41 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "8.3.5" +version = "8.4.1" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, - {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, ] [package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" pluggy = ">=1.5,<2" +pygments = ">=2.7.2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "requests" -version = "2.32.3" +version = "2.32.4" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, ] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" +charset_normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" @@ -631,17 +635,17 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruamel-yaml" -version = "0.18.10" +version = "0.18.14" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "ruamel.yaml-0.18.10-py3-none-any.whl", hash = "sha256:30f22513ab2301b3d2b577adc121c6471f28734d3d9728581245f1e76468b4f1"}, - {file = "ruamel.yaml-0.18.10.tar.gz", hash = "sha256:20c86ab29ac2153f80a428e1254a8adf686d3383df04490514ca3b79a362db58"}, + {file = "ruamel.yaml-0.18.14-py3-none-any.whl", hash = "sha256:710ff198bb53da66718c7db27eec4fbcc9aa6ca7204e4c1df2f282b6fe5eb6b2"}, + {file = "ruamel.yaml-0.18.14.tar.gz", hash = "sha256:7227b76aaec364df15936730efbf7d72b30c0b79b1d578bbb8e3dcb2d81f52b7"}, ] [package.dependencies] -"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.13\""} +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.14\""} [package.extras] docs = ["mercurial (>5.7)", "ryd"] @@ -715,13 +719,13 @@ files = [ [[package]] name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +version = "3.0.1" +description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." optional = false -python-versions = "*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, + {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, + {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, ] [[package]] @@ -944,41 +948,41 @@ files = [ [[package]] name = "typer" -version = "0.15.3" +version = "0.15.4" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" files = [ - {file = "typer-0.15.3-py3-none-any.whl", hash = "sha256:c86a65ad77ca531f03de08d1b9cb67cd09ad02ddddf4b34745b5008f43b239bd"}, - {file = "typer-0.15.3.tar.gz", hash = "sha256:818873625d0569653438316567861899f7e9972f2e6e0c16dab608345ced713c"}, + {file = "typer-0.15.4-py3-none-any.whl", hash = "sha256:eb0651654dcdea706780c466cf06d8f174405a659ffff8f163cfbfee98c0e173"}, + {file = "typer-0.15.4.tar.gz", hash = "sha256:89507b104f9b6a0730354f27c39fae5b63ccd0c95b1ce1f1a6ba0cfd329997c3"}, ] [package.dependencies] -click = ">=8.0.0" +click = ">=8.0.0,<8.2" rich = ">=10.11.0" shellingham = ">=1.3.0" typing-extensions = ">=3.7.4.3" [[package]] name = "typing-extensions" -version = "4.13.2" -description = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.14.1" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, - {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, ] [[package]] name = "typing-inspection" -version = "0.4.0" +version = "0.4.1" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" files = [ - {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, - {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, + {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, + {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, ] [package.dependencies] @@ -986,13 +990,13 @@ typing-extensions = ">=4.12.0" [[package]] name = "urllib3" -version = "2.4.0" +version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" files = [ - {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, - {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ] [package.extras] From 2b5f1449c974011b18a1169ad2e4ff1b9ecc1c78 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Sun, 6 Jul 2025 18:25:34 +0200 Subject: [PATCH 27/40] fix(examples): Fix the example so that it respects the contract --- .../external_module_compilance/spymodel.yml | 4 ++-- .../pipeline_validation/spymodel.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/plugin_based_architecture/external_module_compilance/spymodel.yml b/examples/plugin_based_architecture/external_module_compilance/spymodel.yml index 0dc611f..cd78b33 100644 --- a/examples/plugin_based_architecture/external_module_compilance/spymodel.yml +++ b/examples/plugin_based_architecture/external_module_compilance/spymodel.yml @@ -44,9 +44,9 @@ classes: deployments: - arch: x86_64 systems: - - os: windows + - os: linux pythons: - - version: 3.12.8 + - version: 3.12.9 interpreter: CPython modules: - filename: extension.py diff --git a/examples/plugin_based_architecture/pipeline_validation/spymodel.yml b/examples/plugin_based_architecture/pipeline_validation/spymodel.yml index 9be9d9d..cd78b33 100644 --- a/examples/plugin_based_architecture/pipeline_validation/spymodel.yml +++ b/examples/plugin_based_architecture/pipeline_validation/spymodel.yml @@ -44,9 +44,9 @@ classes: deployments: - arch: x86_64 systems: - - os: windows + - os: linux pythons: - - version: 3.12.8 + - version: 3.12.9 interpreter: CPython modules: - filename: extension.py @@ -63,7 +63,7 @@ deployments: - interpreter: IronPython modules: - filename: addons.py - - os: linux + - os: windows pythons: - version: 3.12.9 interpreter: CPython From 23373c86968f4f96bedb7b6ebda598a5a6545476 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Thu, 31 Jul 2025 12:25:49 +0200 Subject: [PATCH 28/40] chore(docs): migrate documentation from Sphinx to MkDocs --- docs/Makefile | 20 - docs/index.md | 17 + docs/make.bat | 35 - docs/requirements.txt | 10 - docs/source/_static/custom.css | 10 - docs/source/advanced/advanced_index.rst | 43 -- .../advanced/api_reference/api_core.rst | 104 --- .../advanced/api_reference/api_models.rst | 106 --- .../advanced/api_reference/api_utilities.rst | 114 --- .../advanced/api_reference/api_validators.rst | 154 ---- docs/source/advanced/api_reference_index.rst | 45 -- .../architecture_design_decisions.rst | 150 ---- .../architecture/architecture_overview.rst | 114 --- .../architecture_runtime_analysis.rst | 125 ---- .../architecture_validation_engine.rst | 117 --- docs/source/advanced/architecture_index.rst | 64 -- docs/source/beginner/beginner_index.rst | 64 -- docs/source/beginner/poetry_basics.rst | 146 ---- .../source/beginner/pydantic_in_importspy.rst | 126 ---- docs/source/beginner/python_reflection.rst | 121 --- docs/source/conf.py | 66 -- docs/source/get_started.rst | 69 -- docs/source/get_started/example_overview.rst | 65 -- .../external_module_compilance.rst | 114 --- .../plugin_based_architecture/index.rst | 79 -- .../pipeline_validation.rst | 93 --- docs/source/get_started/installation.rst | 87 --- docs/source/index.rst | 74 -- docs/source/overview.rst | 72 -- docs/source/overview/story.rst | 91 --- .../ci_cd_integration.rst | 135 ---- .../contract_structure.rst | 176 ----- .../defining_import_contracts.rst | 156 ---- .../understanding_importspy/embedded_mode.rst | 127 ---- .../error_handling.rst | 153 ---- .../understanding_importspy/error_table.rst | 42 -- .../understanding_importspy/external_mode.rst | 119 --- .../integration_best_practices.rst | 144 ---- .../understanding_importspy/introduction.rst | 113 --- .../spy_execution_flow.rst | 115 --- .../validation_and_compliance.rst | 129 ---- .../understanding_importspy_index.rst | 89 --- .../use_cases/use_case_compilance.rst | 99 --- .../use_cases/use_case_iot_integration.rst | 112 --- .../overview/use_cases/use_case_security.rst | 120 --- .../use_cases/use_case_validation.rst | 99 --- docs/source/overview/use_cases_index.rst | 61 -- docs/source/sponsorship.rst | 68 -- docs/source/vision.rst | 104 --- mkdocs.yml | 1 + poetry.lock | 687 +++++++----------- pyproject.toml | 4 +- 52 files changed, 293 insertions(+), 5055 deletions(-) delete mode 100644 docs/Makefile create mode 100644 docs/index.md delete mode 100644 docs/make.bat delete mode 100644 docs/requirements.txt delete mode 100644 docs/source/_static/custom.css delete mode 100644 docs/source/advanced/advanced_index.rst delete mode 100644 docs/source/advanced/api_reference/api_core.rst delete mode 100644 docs/source/advanced/api_reference/api_models.rst delete mode 100644 docs/source/advanced/api_reference/api_utilities.rst delete mode 100644 docs/source/advanced/api_reference/api_validators.rst delete mode 100644 docs/source/advanced/api_reference_index.rst delete mode 100644 docs/source/advanced/architecture/architecture_design_decisions.rst delete mode 100644 docs/source/advanced/architecture/architecture_overview.rst delete mode 100644 docs/source/advanced/architecture/architecture_runtime_analysis.rst delete mode 100644 docs/source/advanced/architecture/architecture_validation_engine.rst delete mode 100644 docs/source/advanced/architecture_index.rst delete mode 100644 docs/source/beginner/beginner_index.rst delete mode 100644 docs/source/beginner/poetry_basics.rst delete mode 100644 docs/source/beginner/pydantic_in_importspy.rst delete mode 100644 docs/source/beginner/python_reflection.rst delete mode 100644 docs/source/conf.py delete mode 100644 docs/source/get_started.rst delete mode 100644 docs/source/get_started/example_overview.rst delete mode 100644 docs/source/get_started/examples/plugin_based_architecture/external_module_compilance.rst delete mode 100644 docs/source/get_started/examples/plugin_based_architecture/index.rst delete mode 100644 docs/source/get_started/examples/plugin_based_architecture/pipeline_validation.rst delete mode 100644 docs/source/get_started/installation.rst delete mode 100644 docs/source/index.rst delete mode 100644 docs/source/overview.rst delete mode 100644 docs/source/overview/story.rst delete mode 100644 docs/source/overview/understanding_importspy/ci_cd_integration.rst delete mode 100644 docs/source/overview/understanding_importspy/contract_structure.rst delete mode 100644 docs/source/overview/understanding_importspy/defining_import_contracts.rst delete mode 100644 docs/source/overview/understanding_importspy/embedded_mode.rst delete mode 100644 docs/source/overview/understanding_importspy/error_handling.rst delete mode 100644 docs/source/overview/understanding_importspy/error_table.rst delete mode 100644 docs/source/overview/understanding_importspy/external_mode.rst delete mode 100644 docs/source/overview/understanding_importspy/integration_best_practices.rst delete mode 100644 docs/source/overview/understanding_importspy/introduction.rst delete mode 100644 docs/source/overview/understanding_importspy/spy_execution_flow.rst delete mode 100644 docs/source/overview/understanding_importspy/validation_and_compliance.rst delete mode 100644 docs/source/overview/understanding_importspy_index.rst delete mode 100644 docs/source/overview/use_cases/use_case_compilance.rst delete mode 100644 docs/source/overview/use_cases/use_case_iot_integration.rst delete mode 100644 docs/source/overview/use_cases/use_case_security.rst delete mode 100644 docs/source/overview/use_cases/use_case_validation.rst delete mode 100644 docs/source/overview/use_cases_index.rst delete mode 100644 docs/source/sponsorship.rst delete mode 100644 docs/source/vision.rst create mode 100644 mkdocs.yml diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d0c3cbf..0000000 --- a/docs/Makefile +++ /dev/null @@ -1,20 +0,0 @@ -# Minimal makefile for Sphinx documentation -# - -# You can set these variables from the command line, and also -# from the environment for the first two. -SPHINXOPTS ?= -SPHINXBUILD ?= sphinx-build -SOURCEDIR = source -BUILDDIR = build - -# Put it first so that "make" without argument is like "make help". -help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) - -.PHONY: help Makefile - -# Catch-all target: route all unknown targets to Sphinx using the new -# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). -%: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..000ea34 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,17 @@ +# Welcome to MkDocs + +For full documentation visit [mkdocs.org](https://www.mkdocs.org). + +## Commands + +* `mkdocs new [dir-name]` - Create a new project. +* `mkdocs serve` - Start the live-reloading docs server. +* `mkdocs build` - Build the documentation site. +* `mkdocs -h` - Print help message and exit. + +## Project layout + + mkdocs.yml # The configuration file. + docs/ + index.md # The documentation homepage. + ... # Other markdown pages, images and other files. diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100644 index 747ffb7..0000000 --- a/docs/make.bat +++ /dev/null @@ -1,35 +0,0 @@ -@ECHO OFF - -pushd %~dp0 - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set SOURCEDIR=source -set BUILDDIR=build - -%SPHINXBUILD% >NUL 2>NUL -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.https://www.sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "" goto help - -%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -goto end - -:help -%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% - -:end -popd diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index 1777ce4..0000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,10 +0,0 @@ -sphinx>=8.1.3 -sphinx-tabs>=3.4.7 -furo>=2024.8.6 -sphinx-basic-ng>=1.0.0b2 -sphinxcontrib-applehelp>=2.0.0 -sphinxcontrib-devhelp>=2.0.0 -sphinxcontrib-htmlhelp>=2.1.0 -sphinxcontrib-jsmath>=1.0.1 -sphinxcontrib-qthelp>=2.0.0 -sphinxcontrib-serializinghtml>=2.0.0 diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css deleted file mode 100644 index 616a96f..0000000 --- a/docs/source/_static/custom.css +++ /dev/null @@ -1,10 +0,0 @@ -.wy-nav-content { - max-width: 100% !important; - width: 100% !important; -} -/* - -.wy-side-nav-search { - max-width: 20% !important; -} -*/ \ No newline at end of file diff --git a/docs/source/advanced/advanced_index.rst b/docs/source/advanced/advanced_index.rst deleted file mode 100644 index 8eb3642..0000000 --- a/docs/source/advanced/advanced_index.rst +++ /dev/null @@ -1,43 +0,0 @@ -Advanced Topics & Internals of ImportSpy -======================================== - -Welcome to the advanced section of ImportSpy’s documentation — built for developers, integrators, and contributors who want to **go beyond usage** and dive into **how ImportSpy works under the hood**. - -Whether you're building runtime enforcement pipelines, customizing structural validators, or embedding ImportSpy into multi-tenant plugin architectures, this section provides the **deep technical foundation** to unlock ImportSpy's full capabilities. - -🧠 Who This Is For -------------------- - -- Engineers building **custom validation flows** -- Contributors exploring the **internal mechanics** of ImportSpy -- Teams integrating ImportSpy into **CI/CD, containers, and plugin frameworks** -- Architects enforcing **organization-wide import policies** - -🔍 What You’ll Explore ------------------------ - -This section is structured into two complementary areas: - -🏗️ **Architectural Internals** - - A deep technical exploration of ImportSpy’s runtime model, validation stack, and modular design. - - Learn how ImportSpy inspects environments, builds validation contexts, and enforces contracts in both embedded and CLI modes. - -🛠️ **Extension & Integration Points** - - Discover how to write custom validators, extend `SpyModel`, inject runtime policies, or build tooling on top of ImportSpy’s API. - - Ideal for integrating with internal frameworks, policy engines, or advanced CI/CD pipelines. - -📚 **API Reference** - - Browse a fully documented catalog of internal components: - - `SpyModel`, `Function`, `Attribute`, `Deployment`, `Validator`, etc. - - Includes type annotations, usage patterns, and extension strategies. - -This section balances **low-level documentation** with **real-world extensibility guidance**. - -.. toctree:: - :maxdepth: 2 - :caption: Explore the Internals - - architecture_index - api_reference_index - -🚀 Whether you're enforcing security boundaries or writing custom validators, this section is your blueprint for building with — and on top of — ImportSpy. diff --git a/docs/source/advanced/api_reference/api_core.rst b/docs/source/advanced/api_reference/api_core.rst deleted file mode 100644 index afab174..0000000 --- a/docs/source/advanced/api_reference/api_core.rst +++ /dev/null @@ -1,104 +0,0 @@ -Core Engine: Classes, Controllers, and Contracts -================================================ - -This section documents the **core subsystems of ImportSpy** — the internal machinery that powers its runtime validation engine, CLI interface, and contract execution model. - -These APIs are essential for: - -- 🧠 Understanding how validation requests are orchestrated -- 🔄 Hooking into the enforcement lifecycle -- 🛠 Extending ImportSpy for custom validation, logging, or policy enforcement - -Each class below plays a **central role in ImportSpy’s internal flow**, and is fully documented for integration and contribution use cases. - -Spy Class 🕵️‍♂️ -^^^^^^^^^^^^^^^^^ - -The `Spy` class is the **central controller** of ImportSpy’s validation pipeline. - -It handles: - -- Contract parsing and validation -- Import-time introspection of the calling environment -- Runtime orchestration for embedded validation -- Entry point for dynamic execution enforcement - -.. autoclass:: importspy.s.Spy - :members: - :undoc-members: - :show-inheritance: - -Contract Parsers 💾 -^^^^^^^^^^^^^^^^^^^^ - -Import contracts are defined externally as `.yml` files and parsed into structured models. - -- `Parser` is the abstract interface that defines contract loading behavior. -- `YamlParser` is the default parser implementation supporting YAML-based contracts. -- `handle_persistence_error` is a decorator for consistent exception wrapping and traceability. - -.. autoclass:: importspy.persistences.Parser - :members: - :undoc-members: - :show-inheritance: - -.. autoclass:: importspy.persistences.YamlParser - :members: - :undoc-members: - :show-inheritance: - -.. autofunction:: importspy.persistences.handle_persistence_error - -.. autoclass:: importspy.persistences.PersistenceError - :members: - :undoc-members: - :show-inheritance: - -Log Manager 📝 -^^^^^^^^^^^^^^^ - -The `LogManager` provides **structured logging** across both CLI and embedded modes. -It supports log-level control (`DEBUG`, `INFO`, `ERROR`, etc.) and unified message formatting. - -.. autoclass:: importspy.log_manager.LogManager - :members: - :undoc-members: - :show-inheritance: - -Error Messaging ⚠️ -^^^^^^^^^^^^^^^^^^^^ - -The `Errors` class contains standardized error templates used across ImportSpy. -It defines **consistent, user-facing messages** for contract violations, misconfigurations, and environment mismatches. - -.. autoclass:: importspy.errors.Errors - :members: - :undoc-members: - :show-inheritance: - -Constants 📌 -^^^^^^^^^^^^ - -All shared constants, labels, and tags used by the validation engine, parsers, and CLI. -These are used to maintain **naming consistency** across internal modules. - -.. autoclass:: importspy.constants.Constants - :members: - :undoc-members: - :show-inheritance: - -Configuration ⚙️ -^^^^^^^^^^^^^^^^ - -The `Config` class defines **runtime settings and execution context** for ImportSpy. - -It allows developers to: - -- Customize behavior based on validation mode -- Register external contract paths -- Modify enforcement flags dynamically - -.. autoclass:: importspy.config.Config - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/advanced/api_reference/api_models.rst b/docs/source/advanced/api_reference/api_models.rst deleted file mode 100644 index 00334a8..0000000 --- a/docs/source/advanced/api_reference/api_models.rst +++ /dev/null @@ -1,106 +0,0 @@ -Model Layer: SpyModel & Contract Validation System -================================================== - -At the heart of ImportSpy’s compliance framework lies the `SpyModel`: -a fully structured representation of how a Python module **should behave** across different runtime environments. - -ImportSpy does not merely analyze code — it validates whether a module conforms to a **contractual definition** -that includes its **structure**, **runtime expectations**, and **execution constraints**. - -🔍 Whether you’re operating in **embedded mode** or running validations via the **CLI**, -`SpyModel` is the foundation on which all validation logic is built. - -Validation Modes Supported 🧭 ------------------------------ - -Import contracts defined via `.yml` files (or SpyModel objects in code) are evaluated in: - -- **Embedded Mode** 🔌 - Modules protect themselves by invoking `Spy().importspy()` and enforcing contracts on their importers. - -- **External (CLI) Mode** 🛠️ - Used in pipelines or audits to validate a target module before execution or integration. - -Both workflows rely on **SpyModel-based comparison** between expected and actual module states. - -SpyModel Class 🏗️ -------------------- - -The `SpyModel` is a high-level, Pydantic-based model that transforms an import contract into a validated runtime object. - -It defines: - -- 🧱 Structural rules → Expected classes, attributes, methods, return types -- 🧪 Runtime rules → Supported OS, CPU architectures, Python interpreters -- 🔐 Environment rules → Required environment variables and submodule dependencies - -.. autoclass:: importspy.models.SpyModel - :members: - :undoc-members: - :show-inheritance: - -Model Subcomponents 📦 -^^^^^^^^^^^^^^^^^^^^^^^^ - -`SpyModel` is composed of granular submodels that represent the contract’s declarative schema: - -- `Function` → Represents function name, arguments, and return annotations -- `Attribute` → Captures class or global variables (with value, type, and scope) -- `Class` → Groups attributes and methods, along with expected inheritance -- `Module` → Represents nested modules inside deployments -- `Python`, `System`, `Deployment` → Define runtime matrix for cross-platform validation - -.. autoclass:: importspy.models.Function - :members: - :undoc-members: - -.. autoclass:: importspy.models.Attribute - :members: - :undoc-members: - -.. autoclass:: importspy.models.Argument - :members: - :undoc-members: - -.. autoclass:: importspy.models.Class - :members: - :undoc-members: - -.. autoclass:: importspy.models.Module - :members: - :undoc-members: - -.. autoclass:: importspy.models.Python - :members: - :undoc-members: - -.. autoclass:: importspy.models.System - :members: - :undoc-members: - -.. autoclass:: importspy.models.Runtime - :members: - :undoc-members: - -Validator Interface ✅ ----------------------- - -ImportSpy includes a pluggable validator system that compares: - -- The `SpyModel` contract (expected) -- The actual runtime snapshot of the importing environment - -Validators are executed as part of a pipeline that checks: - -- ✔️ Function and method presence -- ✔️ Signature alignment and argument types -- ✔️ Class structure and attribute correctness -- ✔️ Deployment compatibility and environment config -- ✔️ Interpreter and version compliance - -To explore how validators are defined and chained: - -.. toctree:: - :maxdepth: 1 - - api_validators diff --git a/docs/source/advanced/api_reference/api_utilities.rst b/docs/source/advanced/api_reference/api_utilities.rst deleted file mode 100644 index 2002420..0000000 --- a/docs/source/advanced/api_reference/api_utilities.rst +++ /dev/null @@ -1,114 +0,0 @@ -Utilities & Mixins: Internal Tools for Reflection and Runtime Enforcement -========================================================================= - -ImportSpy’s validation engine is powered by a suite of **utility classes** and **mixins** -that enable deep introspection of modules, environments, and Python runtimes. -These components provide the **mechanical backbone** for runtime analysis, structural extraction, -and platform compatibility checks across both **embedded** and **external** validation modes. - -This layer is not typically exposed to end-users—but is invaluable for contributors, -integrators, and advanced developers extending ImportSpy’s logic or building custom tooling. - -Utility Modules ⚙️ ------------------- - -Each utility module encapsulates a specific aspect of **runtime introspection**, enabling: - -- 🔍 Extraction of class/function signatures, annotations, and globals -- 🧠 Detection of system identity (OS, architecture, interpreter, etc.) -- 🔐 Compliance checks for cross-platform and interpreter-specific constraints -- ⚙️ Lightweight, cached evaluation of runtime conditions - -These utilities are **orchestrated automatically** by the validation pipeline but can also -be used independently to write tools or perform standalone validations. - -ModuleUtil – Structural Reflection 🧱 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -This utility performs deep reflection on a Python module, extracting: - -- Public/private classes -- Methods and their argument signatures -- Attribute values and types -- Class hierarchies and superclasses -- Function return annotations - -.. autoclass:: importspy.utilities.module_util.ModuleUtil - :members: - :undoc-members: - :show-inheritance: - -SystemUtil – OS & Environment Detection 🌐 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Gathers platform metadata such as: - -- OS name (`linux`, `windows`, etc.) -- Hostname, architecture -- Environment variable inspection and resolution - -.. autoclass:: importspy.utilities.system_util.SystemUtil - :members: - :undoc-members: - :show-inheritance: - -PythonUtil – Interpreter Validation 🐍 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Identifies the active interpreter and Python version with semantic normalization. -Also verifies whether the environment satisfies version-based or interpreter-based constraints -declared in the import contract. - -.. autoclass:: importspy.utilities.python_util.PythonUtil - :members: - :undoc-members: - :show-inheritance: - -RuntimeUtil – Hardware & Architecture Awareness 🧬 -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Provides a detailed overview of: - -- CPU architecture (`x86_64`, `arm64`, etc.) -- Runtime compatibility with deployment targets -- Cross-architecture filtering logic for contract enforcement - -.. autoclass:: importspy.utilities.runtime_util.RuntimeUtil - :members: - :undoc-members: - :show-inheritance: - -Mixin Components 🔁 -------------------- - -ImportSpy also uses **mixins** to modularize logic that applies across validators and inspectors -without duplicating functionality or creating deep class hierarchies. - -These reusable components inject specialized logic into validation classes where needed. - -AnnotationValidatorMixin 🏷️ -^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Ensures that function signatures and variable annotations match the declared expectations. -It supports: - -- Basic type matching (`str`, `int`, etc.) -- Optional and generic annotations -- Graceful fallback for missing or untyped values - -.. autoclass:: importspy.mixins.annotations_validator_mixin.AnnotationValidatorMixin - :members: - :undoc-members: - :show-inheritance: - -📌 Tip: -------- - -While these components are internal, they can be extended or overridden when customizing -ImportSpy’s validation strategy for highly specific use cases (e.g., custom deployment platforms, -corporate runtime wrappers, or secure import enforcement). - -For more on customizing validation behavior, see: - -- :doc:`../architecture/architecture_design_decisions` -- :doc:`api_validators` diff --git a/docs/source/advanced/api_reference/api_validators.rst b/docs/source/advanced/api_reference/api_validators.rst deleted file mode 100644 index 39384f6..0000000 --- a/docs/source/advanced/api_reference/api_validators.rst +++ /dev/null @@ -1,154 +0,0 @@ -The Validation Engine -===================== - -ImportSpy’s validation system is a modular, extensible framework designed to enforce the integrity -and runtime compatibility of any Python module protected by an **Import Contract**. -Whether used in **embedded validation** (inside the module) or in **external CLI mode**, -this engine guarantees that no module is loaded in an unauthorized or structurally inconsistent environment. - -Core Validator Pipeline ⚙️ --------------------------- - -At the center of the engine is the `SpyModelValidator`, a coordinator that orchestrates multiple specialized validators. -Each validator is responsible for comparing part of the actual runtime context or module structure against its declared expectations. - -This pipeline enforces: - -- ✅ Structural integrity -- ✅ Environment compatibility -- ✅ Runtime reproducibility -- ✅ Compliance with declared variables and system settings - -.. note:: - All validators are executed **at runtime**, just before the module is made accessible to the importing code. - -Validation Modes Supported ---------------------------- - -The validation engine operates seamlessly across both execution modes: - -- **🧬 Embedded Mode** — The module validates its own importer at the moment it is loaded. -- **🛠️ External Mode (CLI)** — The module is validated *before* execution begins, often in CI/CD or static validation workflows. - -SpyModelValidator 🧠 ---------------------- - -This is the **entry point** to the validation pipeline. It receives both: - -- The expected structure (parsed from a `.yml` contract or inline SpyModel), and -- The actual runtime data (extracted via introspection) - -It dispatches these to domain-specific validators, collects results, and raises structured errors on failure. - -.. autoclass:: importspy.validators.spymodel_validator.SpyModelValidator - :members: - :undoc-members: - :show-inheritance: - -Structural Validators 🔎 ------------------------- - -These validators inspect the internal structure of the target module: - -AttributeValidator 🔤 -^^^^^^^^^^^^^^^^^^^^^ - -- Ensures global/module/class attributes exist -- Validates `type`, `value`, and `scope` (class vs instance) -- Supports default values and optional annotations - -.. autoclass:: importspy.validators.attribute_validator.AttributeValidator - :members: - :undoc-members: - :show-inheritance: - -FunctionValidator 🛠️ -^^^^^^^^^^^^^^^^^^^^^ - -- Verifies function/method presence -- Checks signatures and return annotations -- Detects function mismatches across versions or overrides - -.. autoclass:: importspy.validators.function_validator.FunctionValidator - :members: - :undoc-members: - :show-inheritance: - -ArgumentValidator 🎛️ -^^^^^^^^^^^^^^^^^^^^^^ - -- Validates function arguments against contract declarations -- Supports type annotations and default value checks -- Ensures complete function interface compliance - -.. autoclass:: importspy.validators.argument_validator.ArgumentValidator - :members: - :undoc-members: - :show-inheritance: - -Environment & Runtime Validators 🌍 ------------------------------------ - -These components validate that the system attempting to import the module is authorized: - -ModuleValidator 📦 -^^^^^^^^^^^^^^^^^^ - -- Validates module metadata (`filename`, `version`, global `variables`) -- Applies naming constraints defined in contracts - -.. autoclass:: importspy.validators.module_validator.ModuleValidator - :members: - :undoc-members: - :show-inheritance: - -SystemValidator 🖥️ -^^^^^^^^^^^^^^^^^^^ - -- Verifies OS name and version compatibility -- Enforces required `env` variables (e.g., `API_KEY`, `DEPLOY_REGION`) -- Useful for containerized and multi-host setups - -.. autoclass:: importspy.validators.system_validator.SystemValidator - :members: - :undoc-members: - :show-inheritance: - -PythonValidator 🐍 -^^^^^^^^^^^^^^^^^^ - -- Checks that the interpreter matches contract constraints -- Supports semantic Python version matching -- Verifies implementation type (`CPython`, `PyPy`, `IronPython`) - -.. autoclass:: importspy.validators.python_validator.PythonValidator - :members: - :undoc-members: - :show-inheritance: - -RuntimeValidator 🚀 -^^^^^^^^^^^^^^^^^^^^ - -- Validates CPU architecture (`x86_64`, `ARM64`, etc.) -- Filters deployments that must only run on specific hardware targets -- Useful for embedded devices, edge computing, and platform-specific plugins - -.. autoclass:: importspy.validators.runtime_validator.RuntimeValidator - :members: - :undoc-members: - :show-inheritance: - -Extending the Engine 🧩 ------------------------ - -Want to add your own validator? - -- Subclass any validator listed here -- Implement the `.validate()` interface -- Register it manually via `SpyModelValidator` or contract-driven hooks - -This makes ImportSpy ideal for **internal compliance layers**, **custom rule sets**, or **secure enterprise environments**. - -See also: - -- :doc:`api_models` for understanding SpyModel structure diff --git a/docs/source/advanced/api_reference_index.rst b/docs/source/advanced/api_reference_index.rst deleted file mode 100644 index 8ca660e..0000000 --- a/docs/source/advanced/api_reference_index.rst +++ /dev/null @@ -1,45 +0,0 @@ -api_reference_index -=================== - -API Reference: Internals & Extensibility ----------------------------------------- - -This section provides a **complete reference guide** to ImportSpy's internal API — designed for developers and contributors who want to: - -- 🔍 Understand how ImportSpy operates under the hood -- 🛠️ Extend its validation logic with custom models and validators -- ⚙️ Integrate runtime enforcement into existing architectures - -Whether you're writing plugins, debugging structural mismatches, or integrating ImportSpy into a CI/CD pipeline, this reference exposes all the essential **building blocks** behind the framework. - -🧩 What You'll Find Inside ---------------------------- - -🔹 **Core API** - The runtime logic powering ImportSpy’s contract enforcement. - Includes import interceptors, validation orchestration, and execution gating. - -🔹 **Model Layer** - Formal Pydantic-based representations of everything from modules and attributes - to Python interpreters, environments, and deployment matrices. - -🔹 **Utility Layer** - Introspection helpers for analyzing imports, resolving dependencies, - reading metadata, or reflecting on runtime state. - -📚 Each module in this section is fully documented with: -- Class definitions -- Method signatures -- Expected behavior -- Extension guidance -- Real-world usage examples - -.. toctree:: - :maxdepth: 2 - :caption: API Modules - - api_reference/api_core - api_reference/api_models - api_reference/api_utilities - -🧠 Use this reference to go beyond configuration — and shape ImportSpy around your architecture, policies, and execution model. diff --git a/docs/source/advanced/architecture/architecture_design_decisions.rst b/docs/source/advanced/architecture/architecture_design_decisions.rst deleted file mode 100644 index f79d5e7..0000000 --- a/docs/source/advanced/architecture/architecture_design_decisions.rst +++ /dev/null @@ -1,150 +0,0 @@ -Design Principles Behind ImportSpy -================================== - -ImportSpy was built to answer a fundamental question in dynamic Python environments: - -🔐 *"How can we guarantee that modules are imported only under the conditions they were designed for?"* - -Unlike traditional linters or static analyzers, ImportSpy enforces **live structural contracts** at the moment of import. This section details the architectural decisions that enable ImportSpy to operate securely, predictably, and scalably across both **runtime validation** and **automated pipelines**. - -Why Runtime Validation? 🧠 --------------------------- - -Python is dynamic. That’s its strength—and its risk. - -Most tools operate **before execution** (e.g. `mypy`, `flake8`), but these tools can’t: - -- Detect runtime-only configurations (e.g. `os.environ`, `importlib`). -- Block a plugin from loading in an unauthorized host. -- Enforce interpreter or architecture constraints **at runtime**. - -✨ **ImportSpy validates code *as it’s being imported***—right where behavior matters. -It defers enforcement to the **moment of execution**, where guarantees can be *proven*. - -Why Validate the Importing Environment? 🔄 ------------------------------------------- - -ImportSpy inverts the typical validation direction. - -Instead of saying: - -> “This plugin must look like X.” - -It asks: - -> “Is the context trying to use this plugin *safe enough* to do so?” - -Modules don’t just exist—they **run somewhere**. -By inspecting the **caller**, ImportSpy ensures: - -- Plugins are loaded only in verified systems. -- The host respects the structure, env vars, interpreter, etc. -- No unauthorized module can silently consume sensitive logic. - -📌 **This is fundamental to plugin security, cross-runtime compliance, and containerized deployments.** - -Why Declarative Import Contracts? 📜 ------------------------------------- - -Validation logic shouldn’t live inside Python files. - -Instead, ImportSpy uses **external YAML contracts** that describe: - -- Expected runtime constraints (e.g. `os: linux`, `python: 3.12`) -- Structural contracts (functions, classes, attributes) -- Deployment variation (e.g. different rules for Windows vs Linux) - -These contracts are parsed into a runtime `SpyModel`—an internal abstraction built with Pydantic. - -Benefits: - -- ✅ Easy to version -- ✅ Works across both embedded and CLI validation -- ✅ Enables testing, linting, and reuse -- ✅ No invasive logic in the codebase - -Why Python Introspection? 🔍 ----------------------------- - -ImportSpy is powered by Python’s built-in runtime introspection features: - -- `inspect.stack()` to find the caller -- `getmembers()`, `isfunction()`, `isclass()` to rebuild module structure -- `sys`, `platform`, and `os` to gather system metadata - -This allows ImportSpy to: - -- Mirror the structure of any module -- Dynamically analyze real-time execution state -- Validate *what’s really there*—not what’s assumed - -By using native reflection instead of static assumptions, ImportSpy gains **flexibility and truthfulness**. - -Why Two Validation Modes? ⚙️ ------------------------------ - -ImportSpy supports two usage models: - -### 1. Embedded Mode (Validation from inside the module) - -- Great for plugins and reusable components -- Self-protecting modules: reject imports from unverified hosts -- Guarantees runtime safety at every import - -### 2. External Mode (Validation from CI/CD or CLI) - -- Ideal for pre-deployment validation -- Works well in test automation, pipelines, and secure releases -- Ensures modules are structurally valid before execution - -Both use the same contract schema. -Both use the same engine. -Both improve trust. - -Why Block on Failure? 🚫 ------------------------- - -ImportSpy adopts a **zero-trust default**: - -- ❌ Invalid environment? → Raise `ValidationError` -- ❌ Mismatched structure? → Abort import -- ✅ Fully compliant? → Proceed as normal - -This prevents: - -- Unexpected side effects -- Code being “partially valid” -- Runtime surprises in production - -Errors are detailed, categorized, and explain *what failed, where, and why*. - -Performance Tradeoffs & Optimizations 🧮 ----------------------------------------- - -Runtime validation adds overhead. But ImportSpy minimizes it through: - -- 🧠 **Caching** of introspection results -- 🔍 **Selective analysis** (skip unused layers) -- 🧱 **Lazy evaluation** of module components -- 📉 **Short-circuiting** at first contract breach - -Result: validation is fast enough for real-time enforcement—even inside plugins. - -Core Design Principles 🧭 --------------------------- - -These ideas guide ImportSpy’s architecture: - -- **Declarative-first** – Let contracts define validation, not Python logic. -- **Zero-trust imports** – Always verify before executing. -- **Context-aware validation** – Enforce structure *and* environment. -- **Cross-environment readiness** – Design for CI, containers, local, and cloud. -- **Developer ergonomics** – Errors are clear. Contracts are readable. Setup is fast. - -Next Steps 🔬 -------------- - -To see these principles in action: - -- Dive into :doc:`architecture_runtime_analysis` → How runtime environments are captured -- Or explore :doc:`architecture_validation_engine` → How actual validation decisions are made diff --git a/docs/source/advanced/architecture/architecture_overview.rst b/docs/source/advanced/architecture/architecture_overview.rst deleted file mode 100644 index 4e7cfdf..0000000 --- a/docs/source/advanced/architecture/architecture_overview.rst +++ /dev/null @@ -1,114 +0,0 @@ -Architecture Overview -===================== - -ImportSpy is a structural validation engine for Python that operates across two distinct execution models: **embedded mode** and **external (CLI) mode**. -Its architecture is designed to adapt seamlessly to both, providing a **runtime validation system** that enforces **import contracts**—declarative YAML specifications defining how and where a module is allowed to run. - -This section introduces the architectural layers, flows, and principles behind ImportSpy’s execution model. - -Architectural Objectives ------------------------- - -ImportSpy is built upon four core pillars: - -1. **Contract-Driven Validation** - Modules define import contracts that describe the expected runtime and structural context. - -2. **Zero-Trust Execution Model** - Code is never executed unless the importing or imported module complies with declared constraints. - -3. **Dynamic Runtime Enforcement** - System context is reconstructed at runtime using reflection and introspection. - -4. **Composable Validation Layers** - Validation is performed in discrete phases (structure, environment, runtime, interpreter), making the architecture modular and extensible. - -Supported Execution Modes --------------------------- - -ImportSpy is dual-mode by design: - -🔹 **Embedded Mode** (for modules that protect themselves) - -- Validation is triggered **inside** the protected module. -- The module inspects **who is importing it** and verifies the caller’s structure and runtime context. -- Typical use case: plugins that must ensure their importing host complies with an expected contract. - -🔹 **External Mode** (for CI/CD or static compliance pipelines) - -- Validation is triggered via CLI before execution. -- The target module is validated **from the outside**, ensuring it conforms to its declared contract. -- Typical use case: pipeline validation of Python modules before deployment. - -Both modes share the same validation engine and contract semantics but differ in the **direction** of the inspection (who validates whom). - -Architectural Layers --------------------- - -The architecture of ImportSpy can be decomposed into the following logical layers: - -🏗️ **Context Reconstruction Layer** - - Gathers system information from the current runtime. - - Captures OS, Python version, architecture, interpreter, and environment variables. - -🔁 **SpyModel Builder** - - Builds a structured representation of the runtime or module to validate. - - Converts contracts and runtime state into Pydantic models. - -📦 **Import Contract Loader** - - Parses the YAML `.yml` contract into a typed validation model. - - Supports nested structures, deployment variations, and type annotations. - -🔍 **Validation Pipeline** - - Compares the reconstructed runtime or module state against the contract. - - Handles structure (functions, classes), environment (variables), and system (interpreter, OS, arch). - -🔐 **Enforcement & Error Handling** - - Raises structured exceptions on failure (with detailed error classification). - - Blocks execution in embedded mode; returns exit codes in CLI mode. - -Execution Flow --------------- - -📌 Embedded Mode: - -1. Module executes `Spy().importspy(...)` at the top of its source. -2. The call stack is inspected to identify the **caller module**. -3. A `SpyModel` of the caller is reconstructed. -4. The module’s own contract is loaded. -5. If the caller matches the contract, execution continues. -6. If not, a `ValidationError` is raised and execution is blocked. - -📌 External Mode: - -1. CLI is invoked with `importspy -s contract.yml my_module.py`. -2. `my_module.py` is dynamically loaded and introspected. -3. Its structure is extracted: classes, functions, attributes, variables. -4. The YAML contract is parsed into a validation model. -5. Structural and runtime validation is performed. -6. Success → status code 0. Failure → detailed error message and exit code 1. - -Illustration: - -.. image:: https://raw.githubusercontent.com/atellaluca/ImportSpy/main/assets/importspy-architecture.png - :align: center - :alt: ImportSpy Architecture Overview - -Why This Architecture Matters ------------------------------ - -This architecture provides: - -- ✅ Full control over **execution guarantees** of Python modules -- ✅ Runtime enforcement of **environmental and structural policies** -- ✅ Dual-mode support for **plugin protection and CI/CD validation** -- ✅ A uniform validation model across **local, container, and distributed runtimes** - -What’s Next? ------------- - -Continue with: - -- :doc:`architecture_runtime_analysis` → How ImportSpy reconstructs runtime environments -- :doc:`architecture_validation_engine` → The core validation logic and error system -- :doc:`architecture_design_decisions` → Design trade-offs, limitations, and rationale diff --git a/docs/source/advanced/architecture/architecture_runtime_analysis.rst b/docs/source/advanced/architecture/architecture_runtime_analysis.rst deleted file mode 100644 index f4527e0..0000000 --- a/docs/source/advanced/architecture/architecture_runtime_analysis.rst +++ /dev/null @@ -1,125 +0,0 @@ -architecture_runtime_analysis -============================= - -Understanding Runtime Analysis in ImportSpy -------------------------------------------- - -ImportSpy doesn’t guess — it **knows exactly who’s importing your code, from where, and how**. - -Its runtime analysis engine reconstructs the **real-time execution context** surrounding an import and evaluates whether it complies with the expectations declared in an **Import Contract**. - -This section explains how ImportSpy leverages Python's introspection system to **enforce validation dynamically**, across both embedded and CLI modes. - -🧠 What Makes ImportSpy Runtime-Aware? ---------------------------------------- - -At the heart of ImportSpy is a fundamental insight: - -> A Python module isn’t just *defined* — it’s *executed in context.* - -ImportSpy inspects that context to answer questions like: - -- Who is importing this code? -- Is the environment approved (OS, Python version, interpreter)? -- Are expected environment variables and metadata in place? -- Does the runtime architecture match the contract? - -Instead of assuming compliance, ImportSpy **validates the reality of execution** — and blocks code that violates it. - -🧱 Key Layers of Runtime Analysis ----------------------------------- - -Here’s how ImportSpy turns Python’s dynamic nature into a validation pipeline: - -1️⃣ **Call Stack Introspection** - - Uses `inspect.stack()` to trace back to the module attempting the import. - - Identifies the **caller module**, not just the callee. - -2️⃣ **Context Extraction** - - Gathers system metadata, including: - - OS (Linux, macOS, Windows) - - CPU architecture (e.g. `x86_64`, `arm64`) - - Python version and interpreter (CPython, PyPy, IronPython) - - Environment variables (e.g., `API_KEY`, `STAGE`) - - Installed module structure (classes, functions, globals) - -3️⃣ **SpyModel Construction** - - Dynamically builds an internal `SpyModel` from the importing context. - - Matches structure and environment against the import contract. - -4️⃣ **Validation Decision** - - Compares expected constraints from YAML or Python object. - - Raises `ValidationError` if mismatches are found — or returns control if all checks pass. - -🔍 Core Python Tools Behind the Magic --------------------------------------- - -ImportSpy uses only built-in Python modules — no black magic, just introspection: - -- `inspect.stack()` – call stack tracing -- `inspect.getmodule()` – resolve module context -- `platform.system()`, `platform.machine()` – OS and architecture -- `sys.version_info`, `platform.python_implementation()` – Python version and interpreter -- `os.environ` – environment variable resolution -- `getmembers()` – dynamic class/function structure extraction - -These are the building blocks behind ImportSpy’s runtime truth-checking engine. - -⚙️ Embedded vs External Mode: Runtime Differences --------------------------------------------------- - -| Mode | Validated Context | Typical Use Case | -|-----------------|----------------------------|---------------------------------------------------| -| **Embedded** | The **importer** of a module | Plugin architectures, sandbox validation | -| **External** | The **module itself** | CI pipelines, security audits, pre-release checks | - -Both modes rely on the **same runtime model**, but invert the direction of validation. - -✅ Embedded Mode Example: -- `my_plugin.py` calls `import core_module.py` -- Inside `core_module`, validation ensures `my_plugin` is allowed to import it. - -✅ CLI Mode Example: -- You run `importspy -s contract.yml my_plugin.py` -- ImportSpy checks if `my_plugin` complies with its declared structure and runtime constraints. - -🚀 Why Runtime Analysis Changes the Game ------------------------------------------ - -ImportSpy’s runtime model enables features few tools can offer: - -- **Import-time contract enforcement** — with precise control over OS, interpreter, architecture -- **Real context validation** — no assumptions, just introspection -- **Full plugin safety** — modules can reject untrusted importers -- **CI/CD guarantees** — validate module deployment conditions at build time - -Python’s flexibility is often seen as a liability — ImportSpy turns it into an *auditable gate*. - -⚡ Performance Considerations ------------------------------ - -Runtime analysis has a cost — but ImportSpy minimizes it through: - -- **Lazy evaluation** — modules are only analyzed when loaded -- **Context caching** — previously computed SpyModels are reused -- **Selective enforcement** — system modules are skipped unless explicitly targeted - -Validation takes milliseconds, not seconds — even in dynamic plugin workflows. - -🔐 Final Takeaway ------------------- - -ImportSpy’s runtime analysis engine turns **introspection into validation**. - -By deeply understanding **who is importing what, from where, and under which conditions**, ImportSpy enforces: - -✅ Structural correctness -✅ Environmental compliance -✅ Runtime safety — across interpreters, containers, and pipelines - -Whether you’re building a plugin system, securing a package, or hardening your CI, -ImportSpy gives you the tools to **intercept, introspect, and enforce — right at import time.** - -Next: -- :doc:`architecture_validation_engine` → See how the validator pipeline executes -- :doc:`architecture_design_decisions` → Understand the rationale behind ImportSpy’s runtime-first approach diff --git a/docs/source/advanced/architecture/architecture_validation_engine.rst b/docs/source/advanced/architecture/architecture_validation_engine.rst deleted file mode 100644 index 681bbe5..0000000 --- a/docs/source/advanced/architecture/architecture_validation_engine.rst +++ /dev/null @@ -1,117 +0,0 @@ -The Validation Engine: Import-Time Assurance for Python -======================================================= - -At the center of ImportSpy lies its **validation engine** — a layered, runtime-first mechanism designed to make sure that: - -✅ Code only runs in verified environments -✅ Structure and behavior match declared expectations -✅ Unauthorized imports are blocked at the boundary - -Unlike static linters or test suites, ImportSpy runs at **import time**, ensuring that modules are **never executed unless compliant** — a zero-trust posture for the Python ecosystem. - -🎯 What the Validation Engine Actually Does -------------------------------------------- - -The validation engine intercepts import events and answers: - -- Is the importing environment trusted? -- Is the runtime (OS, architecture, interpreter) allowed? -- Does the structure of the module match what was promised? -- Are declared environment variables, dependencies, and APIs present? - -It acts like a **runtime compliance firewall** — catching issues before a single line of code is executed. - -📦 Core Pipeline Stages ------------------------- - -Whether in **embedded** mode, ImportSpy uses the same five-stage validation pipeline: - -1️⃣ **Import Interception** - - Uses stack inspection (`inspect.stack`) to trace the importing module. - - Determines the precise origin of the import. - -2️⃣ **Context Modeling (SpyModel Construction)** - - Builds a full runtime profile: - - OS, CPU architecture - - Python version and interpreter - - Environment variables - - Nested module dependencies - -3️⃣ **Structural Validation** - - Analyzes the module’s actual structure (via `inspect`, `ast`, `getmembers`) - - Compares it against the declared contract: - - Classes and superclasses - - Function names, signatures, return types - - Global variables, attributes, and annotations - -4️⃣ **Contract Evaluation** - - Evaluates the runtime `SpyModel` against the declared import contract (in YAML or Python). - - Uses typed validators to match expected values — with support for optional vs required fields. - -5️⃣ **Enforcement & Feedback** - - ✅ If all checks pass, control is returned to the caller. - - ❌ If validation fails: - - Raise `ValidationError` with structured diagnostics - - Provide exact mismatch detail (missing method, wrong version, etc.) - - Halt execution unless soft mode is enabled - -🔍 Modular Validation Subsystems ---------------------------------- - -ImportSpy’s engine is composed of distinct layers, each with its own responsibility: - -🔹 **Import Interceptor** - Detects runtime context at the moment of import. Gathers caller identity and call stack. - -🔹 **SpyModel Generator** - Constructs a normalized model from dynamic runtime inputs. Represents the environment as data. - -🔹 **Validator Stack** - Runs a pipeline of validators, including: - - Structural validators (classes, functions, attributes) - - Environmental validators (OS, Python, architecture) - - Context validators (importer identity, variables, contract location) - -🔹 **Report Engine** - Formats failure messages and traces: - - Uses centralized error codes - - Offers developer-facing hints and CI-friendly logs - -🔹 **Resolution Manager** (Planned) - In future releases, this will support: - - Auto-suggestions for mismatches - - Soft warnings for dry runs - - Contract diffing and explainability tools - -⚙️ Optimizing for Runtime Performance --------------------------------------- - -Validation must be precise — but also fast. ImportSpy uses: - -- **Lazy Evaluation** – modules are only analyzed when accessed. -- **Context Caching** – avoids recomputing runtime metadata. -- **Selective Enforcement** – skips system libraries and only enforces contracts for targeted modules. -- **Failure Short-Circuiting** – stops on the first critical violation unless configured otherwise. - -In most use cases, validation completes in under 50ms — fast enough for production use, even inside plugin systems. - -🔐 Why This Matters --------------------- - -Python offers no guardrails by default. Anyone can import anything, in any context. - -ImportSpy's validation engine creates those guardrails by: - -✅ Binding module behavior to structural truth -✅ Locking execution to trusted environments -✅ Giving developers and systems **predictable, explainable outcomes** - -It’s the difference between _hoping your module runs correctly_ and _knowing that it only ever runs under the right conditions._ - -📘 Next Steps -------------- - -Continue exploring the architecture: - -- :doc:`architecture_runtime_analysis` → See how execution context is captured -- :doc:`architecture_design_decisions` → Understand the philosophy behind runtime validation diff --git a/docs/source/advanced/architecture_index.rst b/docs/source/advanced/architecture_index.rst deleted file mode 100644 index edeb632..0000000 --- a/docs/source/advanced/architecture_index.rst +++ /dev/null @@ -1,64 +0,0 @@ -ImportSpy Architecture -====================== - -The Internal Blueprint of Runtime Validation 🧠 ------------------------------------------------ - -ImportSpy is more than a module linter — it is an **import-time enforcement layer** -that introduces structural awareness and compliance validation directly into the Python runtime. - -This section explores **how ImportSpy works under the hood**, breaking down its core architecture -into modular layers that combine **dynamic reflection**, **declarative contracts**, and **runtime interception**. - -Why Architecture Matters -------------------------- - -In a Python ecosystem where: - -- Modules are shared across microservices and containers, -- Plugins are authored by third parties, -- Deployments span heterogeneous systems, - -...you need more than just "tests". You need a **validation engine** that adapts at runtime. - -ImportSpy was designed to: - -- 🛡️ **Enforce predictable structure** in external modules -- 🧩 **Capture and interpret runtime conditions** dynamically -- 🔒 **Prevent misaligned or unauthorized integrations** - -It introduces formal boundaries where Python has none. - -What You'll Learn in This Section 📚 ------------------------------------- - -This section explains how ImportSpy brings **declarative rigor to dynamic Python environments**. - -You’ll explore: - -- ✅ The **layered architecture** that enables flexible yet strict validation -- ✅ The **rationale behind each design decision** — from using YAML contracts to stack inspection -- ✅ The **engine that drives compliance enforcement**, based on Pydantic and reflection -- ✅ The **runtime analyzer** that reconstructs execution environments -- ✅ The **performance patterns** that make ImportSpy usable even at scale - -.. toctree:: - :maxdepth: 2 - - architecture/architecture_overview - architecture/architecture_design_decisions - architecture/architecture_validation_engine - architecture/architecture_runtime_analysis - -Who Is This For? ----------------- - -Whether you're: - -- a **developer** embedding ImportSpy in a plugin framework, -- a **security engineer** hardening Python execution boundaries, -- or a **contributor** improving contract modeling, - -this section will give you the architectural grounding to wield ImportSpy **confidently and effectively**. - -Ready to look inside the engine? Let’s go. 🚀 diff --git a/docs/source/beginner/beginner_index.rst b/docs/source/beginner/beginner_index.rst deleted file mode 100644 index c072445..0000000 --- a/docs/source/beginner/beginner_index.rst +++ /dev/null @@ -1,64 +0,0 @@ -Beginner Guide to ImportSpy -=========================== - -👋 Welcome to the **Beginner Guide** for ImportSpy! - -This section is built for developers who are **new to ImportSpy** and want to build a **deep and practical understanding** -of how it works—from its internal architecture to the powerful Python concepts it builds upon. - -ImportSpy is more than just a validation tool—it’s a teaching opportunity. -By understanding its foundations, you’ll not only use it better, but you’ll also sharpen your overall Python skills. - -What You’ll Learn in This Guide 📚 ----------------------------------- - -This guide is designed to help you understand **how ImportSpy works under the hood**, -by introducing you to the core technologies and design principles it leverages. - -🧠 Topics include: - -- **🔧 Managing ImportSpy with Poetry** - Understand how ImportSpy is structured, installed, and maintained using [**Poetry**](https://python-poetry.org/), - a modern dependency manager and project builder for Python. - -- **🔍 Python Reflection & Introspection** - Learn how ImportSpy uses Python’s dynamic features like `inspect`, `importlib`, and stack frames - to reconstruct runtime context and validate imports on the fly. - -- **📐 Pydantic and Data Modeling** - Explore how ImportSpy uses **Pydantic** to define and validate import contracts, - turning YAML declarations into structured, type-safe models that enforce correctness. - -Who Should Read This Guide? 🎯 ------------------------------- - -You’ll benefit most from this guide if: - -✅ You’re **new to ImportSpy** and want to understand how it really works -✅ You’re interested in Python's **runtime system, reflection, and data validation** -✅ You want to learn how **modern tooling like Poetry and Pydantic** can help you write better software - -How to Use This Guide 🛠️ -------------------------- - -Each section in this guide is designed to be self-contained, but together they provide a **progressive learning path**. -We recommend reading them in order, especially if you're new to: - -- Poetry and dependency management in Python -- Reflection and runtime validation -- Declarative contracts and schema-driven design - -Each topic includes: - -- ✅ Clear explanations -- 💡 Real-world examples -- 🧠 Best practices you can apply to your own projects - -.. toctree:: - :maxdepth: 2 - - poetry_basics - python_reflection - pydantic_in_importspy - -🎉 Let’s get started—build your knowledge and unlock the full power of ImportSpy! diff --git a/docs/source/beginner/poetry_basics.rst b/docs/source/beginner/poetry_basics.rst deleted file mode 100644 index 24268b9..0000000 --- a/docs/source/beginner/poetry_basics.rst +++ /dev/null @@ -1,146 +0,0 @@ -Using Poetry with ImportSpy -=========================== - -Poetry is the **official packaging and dependency management tool** used in ImportSpy. -It ensures reproducibility, streamlines development workflows, and enables better collaboration. -This guide will help you understand how to use Poetry within ImportSpy’s ecosystem and learn why it’s essential. - -Why Poetry? ------------ - -Poetry offers a modern alternative to legacy tools like `pip`, `setup.py`, and `requirements.txt`. -It provides: - -- ✅ **Isolated virtual environments** with automatic activation -- ✅ **Declarative dependency management** via `pyproject.toml` -- ✅ **Lockfile consistency** with `poetry.lock` -- ✅ **Integrated build and publishing workflow** -- ✅ **Support for multiple dependency groups** (dev, docs, ci, etc.) - -Installing Poetry ------------------ - -You can install Poetry with the official script: - -.. code-block:: bash - - curl -sSL https://install.python-poetry.org | python3 - - -Verify installation: - -.. code-block:: bash - - poetry --version - -Setting Up ImportSpy --------------------- - -1. Clone the repository: - - .. code-block:: bash - - git clone https://github.com/atellaluca/importspy.git - cd importspy - -2. Install all project dependencies: - - .. code-block:: bash - - poetry install - -3. Activate the virtual environment (optional): - - .. code-block:: bash - - poetry shell - -Dependency Management ---------------------- - -Add dependencies: - -.. code-block:: bash - - poetry add pydantic - poetry add --group dev pytest - -Remove dependencies: - -.. code-block:: bash - - poetry remove pydantic - -Update dependencies: - -.. code-block:: bash - - poetry update # Update all - poetry update pydantic # Update a specific one - -Best practice: -✅ Always commit `poetry.lock` to your VCS to ensure reproducibility. - -Understanding the `pyproject.toml` ----------------------------------- - -.. code-block:: toml - - [tool.poetry] - name = "importspy" - version = "0.2.0" - description = "A validation and compliance framework for Python modules." - authors = ["Luca Atella "] - - [tool.poetry.dependencies] - python = "^3.10" - pydantic = "^2.9.2" - - [tool.poetry.group.dev.dependencies] - pytest = "^8.3.3" - - [tool.poetry.group.docs.dependencies] - sphinx = "^7.2" - furo = "^2024.8.6" - - [tool.poetry.scripts] - importspy = "importspy.cli:validate" - -To run CLI commands defined in the `pyproject.toml`: - -.. code-block:: bash - - poetry run importspy --help - -Versioning and Releases ------------------------ - -ImportSpy follows Semantic Versioning (SemVer). -You can bump versions like this: - -.. code-block:: bash - - poetry version patch | minor | major - -Build and publish (requires authentication): - -.. code-block:: bash - - poetry build - poetry publish - -Exporting Requirements ----------------------- - -If you need a `requirements.txt` (e.g., for Docker or legacy tooling): - -.. code-block:: bash - - poetry export -f requirements.txt --output requirements.txt - -Next Steps ----------- - -Now that you’ve configured Poetry, continue learning about ImportSpy’s internals: - -- :doc:`python_reflection` -- :doc:`pydantic_in_importspy` diff --git a/docs/source/beginner/pydantic_in_importspy.rst b/docs/source/beginner/pydantic_in_importspy.rst deleted file mode 100644 index aca6355..0000000 --- a/docs/source/beginner/pydantic_in_importspy.rst +++ /dev/null @@ -1,126 +0,0 @@ -Pydantic in ImportSpy -====================== - -Why Pydantic Matters for ImportSpy 🧠 -------------------------------------- - -ImportSpy uses **Pydantic** as the foundation for its validation engine, enabling it to model and enforce strict structural and environmental expectations. - -In a dynamic language like Python, where anything can change at runtime, Pydantic provides **deterministic enforcement** of expected module attributes, function signatures, return types, and environment variables. - -By wrapping all validation logic in **Pydantic-based models**, ImportSpy transforms flexible contracts into **strict runtime guards**. - -Core Advantages: - -- ✅ Declarative schemas that model module structure and runtime constraints. -- ✅ Precise, readable errors that help developers fix violations quickly. -- ✅ Built-in support for complex types, enums, environment parsing, and more. - -How Pydantic Is Used in ImportSpy 🔍 ------------------------------------- - -All import contracts (`.yml`) are parsed and converted into nested **Pydantic models** during runtime or CLI validation. These models serve as the "expected shape" against which a module or runtime is validated. - -Each layer of the import contract is mapped to a Pydantic model: - -- A class like `Extension` in a plugin? → `ClassModel`. -- A function like `add_extension(msg: str) -> str`? → `FunctionModel`. -- An interpreter requirement? → `InterpreterModel`. -- OS/environment constraints? → `SystemModel`. - -.. code-block:: python - - from pydantic import BaseModel - from typing import List, Optional - - class MethodModel(BaseModel): - name: str - arguments: List[str] - return_annotation: Optional[str] = None - - class ClassModel(BaseModel): - name: str - methods: List[MethodModel] - -Validation Example 🧪 ----------------------- - -Here’s a simplified validation use case. - -.. code-block:: python - - class PluginContract(BaseModel): - filename: str - classes: List[ClassModel] - - contract = PluginContract( - filename="extension.py", - classes=[ - ClassModel( - name="Extension", - methods=[ - MethodModel(name="add_extension", arguments=["self", "msg"], return_annotation="str") - ] - ) - ] - ) - -Now at runtime, if a module lacks that method or returns the wrong type, ImportSpy fails **before** execution. - -Runtime Failures Are Structured ⚠️ ----------------------------------- - -Pydantic errors are deeply integrated with ImportSpy’s logging and debugging layers: - -.. code-block:: json - - [ - { - "loc": ["classes", 0, "methods", 0, "return_annotation"], - "msg": "str type expected", - "type": "type_error.str" - } - ] - -This means: no silent failures, no vague logs. -You know *exactly* what’s missing, and where. - -Benefits Beyond Type Checking ✅ --------------------------------- - -- 🧩 **Cross-layer schema validation**: classes within modules, methods within classes, etc. -- 🛡️ **Zero-Trust enforcement**: if something’s missing, execution is blocked. -- 🔄 **Reusable contract definitions**: models are consistent across embedded and CLI mode. -- 📖 **Documentation as code**: import contracts double as machine- and human-readable specs. - -Advanced Use: Dynamic Constraints ---------------------------------- - -Want to block execution in certain Python versions? Or only allow certain interpreters? - -Pydantic makes it easy to write declarative rules: - -.. code-block:: python - - from pydantic import BaseModel, validator - - class PythonRuntime(BaseModel): - version: str - - @validator("version") - def must_be_310_or_higher(cls, v): - if v < "3.10": - raise ValueError("Python version must be >= 3.10") - return v - -Conclusion 🎯 -------------- - -Pydantic is not just a convenience in ImportSpy — it’s the **core engine** behind runtime validation. - -It provides a robust layer to define, enforce, and debug structural rules with confidence. - -Next steps: - -- :doc:`python_reflection` — Learn how ImportSpy introspects code dynamically. -- https://docs.pydantic.dev/ — Go deeper into advanced Pydantic use cases. diff --git a/docs/source/beginner/python_reflection.rst b/docs/source/beginner/python_reflection.rst deleted file mode 100644 index 72e4c84..0000000 --- a/docs/source/beginner/python_reflection.rst +++ /dev/null @@ -1,121 +0,0 @@ -Understanding Python Reflection in ImportSpy -============================================ - -Why Reflection Matters 🪞 --------------------------- - -Python's reflection capabilities allow code to inspect, analyze, and interact with itself at runtime. -This is central to how **ImportSpy** validates modules dynamically — it doesn't just look at source code, -it actively examines **what exists and how it behaves** at the moment of import. - -In a system where plugins or modules are loosely coupled, this allows ImportSpy to: - -- Validate structural expectations (`classes`, `functions`, `attributes`). -- Detect runtime constraints (`interpreter`, `version`, `environment`). -- Prevent unexpected or unauthorized imports. - -Core Python Reflection Tools 🔍 -------------------------------- - -ImportSpy uses several key components of Python’s reflection toolbox: - -**1. `inspect`** — Runtime introspection - -.. code-block:: python - - import inspect - - def foo(): pass - - print(inspect.isfunction(foo)) # True - print(inspect.getmembers(foo)) # List all members of the function object - -**2. `getattr` / `hasattr` / `setattr`** — Attribute access and mutation - -.. code-block:: python - - class User: name = "Alice" - - u = User() - print(getattr(u, "name")) # "Alice" - print(hasattr(u, "email")) # False - setattr(u, "email", "a@example.com") # Dynamically add attribute - -**3. `importlib`** — Dynamic module loading - -.. code-block:: python - - import importlib - - mod = importlib.import_module("math") - print(mod.sqrt(16)) # 4.0 - -These techniques allow ImportSpy to analyze **any arbitrary Python module** during validation. - -How ImportSpy Uses Reflection 🧠 --------------------------------- - -ImportSpy doesn’t hardcode validation rules into your code. -Instead, it reads a YAML contract, parses it into a structured `SpyModel`, and: - -1. **Intercepts the importing context** - → via `inspect.stack()` to determine *who* is importing the validated module. - -2. **Loads the target module** - → via `importlib` or by extracting from `sys.modules`. - -3. **Validates its structure** - → using `inspect.getmembers()` to check for methods, annotations, and base classes. - -4. **Checks runtime environment** - → including Python version, interpreter type, and required variables. - -This **dynamic, contract-driven validation** is only possible thanks to Python's reflective architecture. - -Reflection in Embedded Mode vs CLI Mode 🔁 ------------------------------------------- - -In **Embedded Mode**, reflection is used by the validated module itself: - -- It calls `Spy().importspy(...)` -- Uses `inspect.stack()` to identify the **caller** -- Then validates that external environment using reflection - -In **CLI Mode**, reflection is applied directly to the target file: - -- `importspy -s contract.yml module.py` -- ImportSpy dynamically loads and introspects the module -- Checks all runtime constraints before it can be deployed - -Best Practices & Pitfalls ⚠️ ----------------------------- - -Reflection is powerful — but should be used wisely: - -✅ **Cache inspection results** to avoid repeat analysis -❌ Avoid calling unknown or unsafe methods with `getattr()` blindly -✅ Combine with type checks (`callable`, `isinstance`) before execution -❌ Don’t mutate live objects unless you're in full control - -Example: safe method invocation - -.. code-block:: python - - if hasattr(module, "run") and callable(module.run): - module.run() - -Takeaway 🧠 ------------ - -Reflection is what makes ImportSpy possible. - -By using `inspect`, `importlib`, and Python’s runtime model, ImportSpy can: - -- Enforce validation without altering your code -- Dynamically adapt to different environments -- Offer a robust, runtime-safe contract enforcement system - -Explore more: - -- :doc:`pydantic_in_importspy` -- `https://docs.python.org/3/library/inspect.html` diff --git a/docs/source/conf.py b/docs/source/conf.py deleted file mode 100644 index bd90af1..0000000 --- a/docs/source/conf.py +++ /dev/null @@ -1,66 +0,0 @@ -# Configuration file for the Sphinx documentation builder. -# -# For the full list of built-in configuration values, see the documentation: -# https://www.sphinx-doc.org/en/master/usage/configuration.html - -# -- Project information ----------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information - -project = 'ImportSpy' -copyright = '2024, Luca Atella' -author = 'Luca Atella' -release = '0.3.3' - -# -- General configuration --------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration - -extensions = [ - 'sphinx.ext.napoleon', - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx_tabs.tabs', -] - -templates_path = ['_templates'] -exclude_patterns = [] - - - -# -- Options for HTML output ------------------------------------------------- -# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output - -html_theme = "furo" -html_static_path = ['_static'] -html_css_files = ['custom.css'] - -html_theme_options = { - -} - -html_theme_options["footer_icons"] = [ - { - "name": "GitHub", - "url": "https://github.com/atellaluca/ImportSpy", - "html": """ - - - - - """, - "class": "", - }, - { - "name": "PyPI", - "url": "https://pypi.org/project/ImportSpy/", - "html": """ - - - - """, - "class": "", - } -] diff --git a/docs/source/get_started.rst b/docs/source/get_started.rst deleted file mode 100644 index 76e1b5a..0000000 --- a/docs/source/get_started.rst +++ /dev/null @@ -1,69 +0,0 @@ -Get Started with ImportSpy 🚀 -============================= - -Welcome to the official **Get Started** guide for ImportSpy! - -This section is your hands-on introduction to ImportSpy’s capabilities and philosophy. -If you're building systems with **plugins**, **microservices**, **modular designs**, or simply want **more control over how your code is imported**, you're in the right place. - -ImportSpy brings structure and validation to Python's dynamic import system — allowing you to **enforce strict module contracts**, detect **runtime mismatches**, and ensure that every imported module behaves exactly as expected. - -What You’ll Learn 📘 ---------------------- - -This guide walks you through everything you need to **set up, understand, and use ImportSpy effectively**, including: - -- 🧰 **Installation** - Learn how to install ImportSpy using your preferred package manager and verify your setup. - -- 🔍 **Example Overview** - Explore a real-world use case built around a **plugin-based architecture**, and understand how ImportSpy adds reliability to it. - -- 📜 **Import Contracts in YAML** - Understand how ImportSpy uses `.yml` contracts to define what an external module must look like: classes, methods, attributes, environments, and more. - -- ⚙️ **Code Walkthrough** - Dive into a fully working example and see how ImportSpy enforces contracts dynamically at runtime. - -- 🧪 **Validation in Action** - Run the example locally and observe how ImportSpy validates external modules and reports violations clearly and precisely. - -Is This Guide for You? ------------------------ - -Absolutely — if any of these apply to you: - -- ✅ You’ve never used ImportSpy before and want a guided path -- ✅ You work with Python plugins, extensions, or modular projects -- ✅ You want to ensure that external code is predictable and secure -- ✅ You're interested in enforcing structure and compliance at runtime - -What You’ll Need 🛠️ --------------------- - -- Basic familiarity with Python and importing modules -- **Python 3.10 or later** installed on your machine -- A terminal or shell to execute ImportSpy from the command line (optional for CLI mode) -- A willingness to explore and experiment! - -Let’s Build Your First Validated Project ✅ -------------------------------------------- - -In the next steps, you’ll learn how to: - -1. Install ImportSpy in seconds -2. Define your first import contract -3. Validate an external module in both embedded and CLI mode -4. Understand how ImportSpy reacts to structural mismatches -5. Integrate it into a real project - -By the end of this guide, you’ll have a **fully functional ImportSpy environment**, understand the power of import contracts, and feel confident applying them in your own architecture. - -Let’s get started! - -.. toctree:: - :maxdepth: 2 - - get_started/installation - get_started/example_overview - get_started/examples/plugin_based_architecture/index diff --git a/docs/source/get_started/example_overview.rst b/docs/source/get_started/example_overview.rst deleted file mode 100644 index f9c815c..0000000 --- a/docs/source/get_started/example_overview.rst +++ /dev/null @@ -1,65 +0,0 @@ -ImportSpy Examples: Real-World Scenarios in Action 🚀 -===================================================== - -Welcome to the **Examples** section of ImportSpy — where theory meets practice. - -In this space, you'll explore **runnable, real-world demonstrations** showing how ImportSpy helps enforce **structural validation**, **interface consistency**, and **runtime compliance** in modular Python systems. - -Whether you're working with plugins, pipelines, APIs, or layered architectures, these examples provide **blueprints you can adapt** to your own projects. - -Why Examples Matter 🧩 ------------------------ - -Modern Python applications are highly dynamic. -But that flexibility comes with risks: unexpected behaviors, silent failures, and integration mismatches. - -ImportSpy gives you a way to bring **formal guarantees** into the dynamic world of imports. -Here, you'll see exactly how it works — with practical, minimal, and extensible examples. - -How to Use These Examples ⚙️ ------------------------------- - -Each example in this section is: - -- ✅ Self-contained and ready to run -- ✅ Designed around real architectural patterns -- ✅ Focused on one validation principle at a time -- ✅ Ideal for experimentation and adaptation - -To try them: - -1. Ensure you have **ImportSpy installed** - .. code-block:: bash - - pip install importspy - -2. Choose an example that matches your context -3. Run it locally -4. Modify the contract or the code and observe the validation outcomes -5. Learn how ImportSpy blocks invalid imports and reinforces structural safety - -Available Example 💡 ---------------------- - -The first complete walkthrough is: - -📦 :doc:`examples/plugin_based_architecture/index` - -This scenario demonstrates: - -- Defining import contracts -- Handling plugin structure enforcement -- Using both **embedded** and **CLI** validation modes -- Running validation in a pipeline context - -Coming Soon ✨ --------------- - -We're actively expanding this section with more examples, including: - -- API structure validation (FastAPI, Flask) -- Cross-service contract enforcement in microservices -- Schema enforcement in data pipelines -- Security policies for runtime imports - -Want to contribute your own? Reach out or open a PR on GitHub! diff --git a/docs/source/get_started/examples/plugin_based_architecture/external_module_compilance.rst b/docs/source/get_started/examples/plugin_based_architecture/external_module_compilance.rst deleted file mode 100644 index 6753fc9..0000000 --- a/docs/source/get_started/examples/plugin_based_architecture/external_module_compilance.rst +++ /dev/null @@ -1,114 +0,0 @@ -External Module Compliance (Embedded Mode Example) -================================================== - -This example demonstrates one of ImportSpy’s most powerful features: -**embedded validation**, where a module being imported can validate **who is importing it**. - -Unlike traditional tools that validate their own structure, ImportSpy allows a module to **control its consumers** — -ensuring that only fully compliant modules can interact with it. - -Why This Matters 🔐 --------------------- - -In plugin architectures, dynamic systems, or modular platforms, core components are often imported by untrusted or external code. -Without structural guarantees, this opens the door to: - -- Runtime crashes from missing methods -- Silent logic errors due to incompatible extensions -- Unpredictable behaviors across environments - -ImportSpy solves this by allowing the core module to define a **YAML-based contract**, and reject importers that don’t match. - -Use Cases ✅ -~~~~~~~~~~~~ - -- Plugin systems with strict APIs -- Modular backends with third-party integration -- Secure extensions and validation gateways -- Projects needing **controlled extensibility** from external modules - -Project Structure 📁 ---------------------- - -.. code-block:: - - external_module_compliance/ - ├── extension.py # External module trying to import the core - ├── package.py # Core module protected by ImportSpy - ├── plugin_interface.py # Shared interface definition - └── spymodel.yml # Structural contract for external validation - -How It Works 🧠 ----------------- - -1. `extension.py` tries to import `package.py` -2. Inside `package.py`, ImportSpy runs in **embedded mode**: - .. code-block:: python - - caller_module = Spy().importspy(filepath="spymodel.yml") - -3. The contract in `spymodel.yml` defines what `extension.py` must contain (e.g., classes, methods, variables) -4. If the contract is satisfied: - - `caller_module` is assigned to `extension.py` - - The validated importer can be used directly, like: - `caller_module.Foo().get_bar()` -5. If not, ImportSpy raises an error and **prevents usage of the module**. - -Run the Example ▶️ --------------------- - -From the root of the project, run: - -.. code-block:: bash - - cd examples/plugin_based_architecture/external_module_compliance - python extension.py - -Expected Output: - -.. code-block:: text - - Foobar - -This means: - -- The importer (`extension.py`) passed validation -- The core module (`package.py`) verified its importer before doing anything -- You now have **runtime-level confidence** in how the system integrates - -Simulating a Failure ❌ ------------------------- - -To see ImportSpy in action, try this: - -1. Open `spymodel.yml` -2. Modify a method name (e.g., `add_extension` → `add_extension_WRONG`) -3. Run the example again: - -.. code-block:: bash - - python extension.py - -Expected output: - -.. code-block:: text - - ValueError: Missing method in class Extension: 'add_extension_WRONG'. Ensure it is defined. - -🛑 This is **real-time structural enforcement**. -The module is immediately blocked for violating the import contract. - -Key Takeaways 🧩 ------------------ - -- ImportSpy’s **embedded mode** empowers a module to **control who is allowed to import it** -- It guarantees that plugins, extensions, or third-party modules conform to the contract before any code runs -- The returned `caller_module` gives you full access to the validated importer — just like any other module object -- This pattern is ideal when **predictability, structure, and security** are non-negotiable - -Next Steps 🔄 -------------- - -- Try editing the contract and module to explore different validations -- Combine this with :doc:`pipeline_validation` to enforce contracts in CI/CD pipelines -- Read more about embedded mode in :doc:`../../../overview/understanding_importspy/embedded_mode` diff --git a/docs/source/get_started/examples/plugin_based_architecture/index.rst b/docs/source/get_started/examples/plugin_based_architecture/index.rst deleted file mode 100644 index ef1c92a..0000000 --- a/docs/source/get_started/examples/plugin_based_architecture/index.rst +++ /dev/null @@ -1,79 +0,0 @@ -Plugin-Based Architecture: Example Suite Overview -================================================= - -Welcome to the **Plugin-Based Architecture** examples for ImportSpy. -This section showcases how ImportSpy can be integrated into real-world modular systems to ensure **structural integrity**, **runtime compatibility**, and **import-time compliance**. - -Why Plugins Need Validation 🧩 ------------------------------- - -Modern applications are often built around **plugin systems**, **modular services**, or **runtime extensions** — -components that are loaded dynamically and sometimes authored externally. - -Without strict validation, these integrations can lead to: - -- ❌ Unexpected runtime errors -- ❌ Silent logic bugs due to mismatched interfaces -- ❌ Security vulnerabilities in dynamic loading scenarios - -ImportSpy solves this by enforcing **formal contracts** — ensuring that every module that is imported or interacted with follows a precise structure and runtime context. - -What You'll Learn Here 🎯 --------------------------- - -In this section, you’ll explore two complementary validation modes: - -.. list-table:: - :widths: 25 75 - :header-rows: 1 - - * - Validation Mode - - Description - * - Embedded Mode - - Validation is performed **inside the core module** (e.g. `package.py`) that is being imported. - When an external module (like `extension.py`) imports it, the core validates the importer. - Ideal for secure plugin frameworks, APIs, or modular applications. - * - CLI Mode - - Validation is performed **externally via the command line**, using `importspy -s contract.yml module.py`. - Perfect for CI/CD, static enforcement, or pre-deployment checks. - -How to Run the Examples 🛠️ ---------------------------- - -Make sure ImportSpy is installed: - -.. code-block:: bash - - pip install importspy - -Then: - -- 🧪 **Embedded Validation** - .. code-block:: bash - - cd examples/plugin_based_architecture - python extension.py - -- 🧪 **CLI Validation** - .. code-block:: bash - - cd examples/plugin_based_architecture - importspy -s spymodel.yml extension.py - -Try editing the modules or the contract and rerun the validations — -you’ll see how ImportSpy detects mismatches immediately. - -Ready to Dive In? 🚀 --------------------- - -These examples provide a practical foundation for using ImportSpy in your own architecture. -They demonstrate not just how validation works, but **where it fits** in modern Python workflows. - -Navigate to a specific mode to explore: - -.. toctree:: - :maxdepth: 1 - :caption: Validation Modes - - external_module_compilance - pipeline_validation diff --git a/docs/source/get_started/examples/plugin_based_architecture/pipeline_validation.rst b/docs/source/get_started/examples/plugin_based_architecture/pipeline_validation.rst deleted file mode 100644 index 6c0099d..0000000 --- a/docs/source/get_started/examples/plugin_based_architecture/pipeline_validation.rst +++ /dev/null @@ -1,93 +0,0 @@ -Pipeline Validation (CLI Mode Example) -====================================== - -This example demonstrates how to use **ImportSpy** in **CLI mode** to validate a Python module against a declared import contract. - -In this scenario, validation is **external and decoupled** — the module being validated has no awareness of ImportSpy. -This makes it ideal for **CI/CD pipelines**, **automated pre-deployment checks**, or **manual compliance validation** during code review. - -Why This Mode Is Powerful 🎯 ----------------------------- - -Unlike embedded mode (where the validated module uses ImportSpy internally), -CLI mode allows you to **treat validation as an independent, enforceable policy**. - -This is especially useful when: - -- You’re validating **third-party plugins or contributors' code** -- You want **full separation of concerns** between business logic and validation -- You’re integrating ImportSpy into **automated pipelines** - -Project Structure 📁 ---------------------- - -.. code-block:: - - pipeline_validation/ - ├── extension.py # The module to validate - ├── plugin_interface.py # Shared base class expected by the contract - └── spymodel.yml # Contract declaring expected structure and runtime - -How It Works ⚙️ ----------------- - -1. The contract in `spymodel.yml` defines the structure, environment, and runtime context expected from `extension.py` -2. ImportSpy is invoked from the command line to **validate `extension.py` against the contract** -3. If validation passes ✅, the pipeline continues - If it fails ❌, the pipeline halts with an explicit error - -Running the Example ▶️ ------------------------ - -First, make sure ImportSpy is installed: - -.. code-block:: bash - - pip install importspy - -Then run: - -.. code-block:: bash - - cd examples/plugin_based_architecture/pipeline_validation - importspy -s spymodel.yml extension.py - -If the module matches the contract, you’ll see something like: - -.. code-block:: text - - ✅ Validation passed: extension.py complies with contract. - -If it fails, you’ll get a detailed, actionable error: - -.. code-block:: text - - ❌ Validation failed - - Reason: - Missing attribute 'instance' in class 'Extension': extension_instance_name_WRONG - -Try Breaking It 🔧 -------------------- - -To see the validator in action: - -1. Open `spymodel.yml` -2. Change an attribute, method, or variable name (e.g., `add_extension` → `add_extension_WRONG`) -3. Run the command again -4. ImportSpy will immediately detect the structural mismatch and explain why - -Key Takeaways 💡 ------------------ - -- **CLI mode** is perfect for validating modules *before execution* -- You can enforce architectural contracts without modifying the validated code -- Works seamlessly in CI/CD pipelines, GitHub Actions, or any build process -- Makes **structural integrity** a core part of your development workflow - -What’s Next? -------------- - -- Try integrating this step into your CI/CD pipeline (e.g., GitHub Actions or GitLab CI) -- Explore :doc:`external_module_compilance` to learn how embedded mode complements this approach -- Read more about CLI mode in :doc:`../../../overview/understanding_importspy/external_mode` diff --git a/docs/source/get_started/installation.rst b/docs/source/get_started/installation.rst deleted file mode 100644 index 9e21d28..0000000 --- a/docs/source/get_started/installation.rst +++ /dev/null @@ -1,87 +0,0 @@ -Installation Guide -================== - -Welcome to the **Installation Guide** for ImportSpy. -This section will walk you through setting up ImportSpy in your environment — quickly, cleanly, and with confidence. - -ImportSpy is designed to be lightweight and easy to integrate into any Python project that values **runtime validation**, **structural compliance**, and **predictable imports**. - -System Requirements 📌 ------------------------ - -Before you begin, make sure your development environment meets the following requirements: - -- **Python 3.10 or later** - ImportSpy relies on modern Python features and guarantees compatibility only from version 3.10 onward. - -- **pip (latest version)** - To ensure smooth installation and dependency resolution. - -- **Virtual Environment (Recommended)** - While optional, using a virtual environment is best practice for avoiding dependency conflicts and ensuring isolation. - -Installing ImportSpy ⚙️ ------------------------- - -1. **Create and Activate a Virtual Environment** - - While not mandatory, we strongly recommend installing ImportSpy in a virtual environment: - - .. tabs:: - - .. tab:: macOS / Linux - - .. code-block:: bash - - python3 -m venv venv - source venv/bin/activate - - .. tab:: Windows - - .. code-block:: bash - - python -m venv venv - .\venv\Scripts\activate - - Once activated, your terminal should indicate that the environment is active. - -2. **Install ImportSpy with pip** - - Now install ImportSpy directly from PyPI: - - .. code-block:: bash - - pip install importspy - - This command will install the latest stable version of ImportSpy and all required dependencies. - -Verifying the Installation ✅ ------------------------------- - -To confirm that ImportSpy is correctly installed and ready to use, run: - -.. code-block:: bash - - importspy --version - -If everything is set up correctly, the terminal will display the current version of ImportSpy. - -Troubleshooting Tips 🧯 ------------------------- - -If something goes wrong: - -- Ensure you're using **Python 3.10+** -- Activate your virtual environment before running `pip install` -- If needed, upgrade pip: - .. code-block:: bash - python -m pip install --upgrade pip - -You're Ready to Go 🎉 ----------------------- - -That’s it! You’re now ready to start using ImportSpy to enforce module validation in your projects. - -Continue to the next section to explore a working example and see ImportSpy in action: - -📎 :doc:`example_overview` diff --git a/docs/source/index.rst b/docs/source/index.rst deleted file mode 100644 index 1f15e31..0000000 --- a/docs/source/index.rst +++ /dev/null @@ -1,74 +0,0 @@ -Welcome to ImportSpy 🔎 -======================== - -**ImportSpy** is a contract-based runtime validation framework that transforms how Python modules interact—making those interactions **predictable, secure, and verifiable**. -It empowers developers to define, enforce, and validate **import contracts** that describe exactly how a module should behave when it is imported, under specific runtime conditions. - -Whether you're working with **plugin-based systems**, **microservices**, or **cross-platform applications**, ImportSpy gives you **full control over integration boundaries**. -It ensures that the modules importing your code—or the ones you're importing—adhere to **explicit structural and environmental rules**, avoiding silent failures, runtime crashes, or unpredictable behavior. - -🔐 ImportSpy is not just about validation—it’s about **bringing discipline and clarity to the most dynamic part of Python: the import system**. - -Why ImportSpy? 🚀 ------------------- - -- **🧩 Bring Structure to Dynamic Systems** - Enforce well-defined contracts on imported modules: classes, methods, variables, versions, OS, interpreters, and more. - -- **🔍 Runtime-Aware Validation** - Validate modules **based on actual runtime context**—OS, CPU architecture, Python interpreter, and version. - -- **🔌 Built for Plugin Ecosystems** - Protect core logic from integration errors in environments where dynamic loading is common. - -- **🧪 Two Powerful Modes** - In **embedded mode**, validate external modules *that import your code*, enforcing structure and context dynamically. - In **CLI mode**, validate any Python module against a contract—ideal for CI/CD pipelines and automated checks. - -- **📜 Self-Documenting Contracts** - The `.yml` contract files double as **live documentation**, formalizing how modules are expected to behave. - -What You'll Learn From This Documentation 📖 --------------------------------------------- - -This guide is designed to help you: - -- Understand how ImportSpy works and **why it exists** -- Learn how to **define and apply import contracts** -- Explore **real-world use cases** across validation, compliance, CI/CD, security, and IoT integration -- Navigate through **beginner-friendly training material** that introduces reflection, Pydantic, Poetry, and more -- Dive into the **internals** of ImportSpy with detailed API references and architectural insights -- Discover how to **support or sponsor the project** to help it grow - -How to Navigate This Documentation 🧭 -------------------------------------- - -- **👋 New to ImportSpy?** → Start with **Get Started** to see how it works, step by step. -- **📚 Want to understand the bigger picture?** → Visit the **Overview** section to explore the vision, story, and use cases. -- **🧠 Curious about internals?** → Explore **Advanced Documentation** for architecture, runtime analysis, and API design. -- **🎓 Need a learning space?** → Head to the **Beginner Section** to explore tools and practices relevant to ImportSpy. -- **💼 Interested in supporting ImportSpy?** → Visit the **Sponsorship** section to learn how to get involved. - -Let’s build Python software that’s not just flexible, but also **reliable, validated, and future-proof**. -**Welcome to the new standard for structural integration in Python.** - -.. toctree:: - :maxdepth: 2 - :caption: 📌 Core Documentation - - vision - overview - get_started - sponsorship - -.. toctree:: - :maxdepth: 2 - :caption: 🎓 Beginner Resources - - beginner/beginner_index - -.. toctree:: - :maxdepth: 2 - :caption: 🧠 Advanced Topics - - advanced/advanced_index diff --git a/docs/source/overview.rst b/docs/source/overview.rst deleted file mode 100644 index be78402..0000000 --- a/docs/source/overview.rst +++ /dev/null @@ -1,72 +0,0 @@ -Overview of ImportSpy -====================== - -Welcome to the **ImportSpy Overview** — a complete starting point for understanding the *why*, *how*, and *where* of this project. - -ImportSpy was born from a clear need: -> How can we bring **predictability**, **security**, and **structural clarity** to Python’s dynamic import system? - -This section explores not only how ImportSpy works, but also **why it exists**, the **real-world problems it solves**, and **what principles it’s built upon**. Whether you’re a developer, architect, or security engineer, this is where your journey begins. - -What You’ll Find in This Section 📖 ------------------------------------ - -This overview is structured into three key parts, each with a distinct purpose: - -The Story Behind ImportSpy -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -At its core, ImportSpy is more than just a tool — it’s the result of a **personal journey**. -Created by Luca Atella as a response to burnout and routine, ImportSpy emerged from a need to **reclaim joy and meaning in development**. -This section tells that story — not for sentiment, but to show that **structure and purpose can coexist in software**. -*It reminds us that even small tools, built from a place of passion, can change the way we work.* - -📄 :doc:`overview/story` - -Use Cases in the Real World -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -ImportSpy is used wherever **modular boundaries need to be enforced** — from plugin ecosystems to CI/CD pipelines. -This section presents **detailed, practical examples** that show how ImportSpy prevents: - -- Misaligned structures in dynamically loaded components -- Security flaws from unvalidated external modules -- Runtime instability across architectures or Python environments - -Use cases include: - -- ✅ **IoT and platform-specific integration** -- ✅ **Validation & structural integrity in plugin systems** -- ✅ **Security enforcement through runtime checks** -- ✅ **Regulatory compliance for mission-critical modules** - -📄 :doc:`overview/use_cases_index` - -Understanding ImportSpy’s Core -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This is your **deep dive** into the internal logic and architecture of ImportSpy. - -You’ll learn: - -- What an **import contract** really is, and how to write one -- How the **Spy validation flow** works from import interception to enforcement -- The difference between **embedded mode** and **CLI mode** -- How runtime context (OS, Python version, architecture) plays a role -- How errors are reported and how to fix them -- Best practices for **CI/CD pipelines and integration at scale** - -*This section turns concepts into confidence, and makes ImportSpy a natural extension of your development process.* - -📄 :doc:`overview/understanding_importspy_index` - -Let’s get started by exploring the motivation, capabilities, and inner workings of ImportSpy — -and discover how it can help you build Python software that’s **modular, compliant, and future-ready**. - -.. toctree:: - :maxdepth: 2 - :caption: 🔍 Overview - - overview/story - overview/use_cases_index - overview/understanding_importspy_index diff --git a/docs/source/overview/story.rst b/docs/source/overview/story.rst deleted file mode 100644 index 6e1e141..0000000 --- a/docs/source/overview/story.rst +++ /dev/null @@ -1,91 +0,0 @@ -From Burnout to Reinvention: The Birth of ImportSpy -=================================================== - -In the relentless world of software development, the passion that once sparked creativity -can slowly be consumed by **repetition, stagnation, and burnout**. -What begins as a thrilling craft may turn into an **unfulfilling routine**, -where problem-solving is replaced by **rote tasks and organizational complexity**. - -**ImportSpy was born from this very struggle.** -It is more than just a validation framework—it’s the product of a **deeply personal journey**, -a project that transformed frustration into clarity and reignited a **genuine love for meaningful software**. - -Losing Passion in a World of Repetition ---------------------------------------- - -For **Luca Atella**, creator of ImportSpy, programming was never just a job. -Like many developers, he started young—captivated by the magic of **turning logic into solutions**, -and the joy of **creating something from nothing**. - -But over time, that joy began to fade. -The once-exciting world of coding gave way to **monotony and constraint**. -Innovation was replaced by **repetitive cycles, boilerplate maintenance, and uninspiring work**. - -At just **24 years old**, Luca made a bold move: he walked away from a stable but soulless job. -A leap into uncertainty, driven by the realization that working without passion was no longer sustainable. -But quitting wasn’t the solution—it was just the start of something deeper: -**a period of rediscovery, doubt, and the difficult question—how do you fall in love with your craft again?** - -Rebuilding Passion, One Line at a Time --------------------------------------- - -The answer wasn’t to abandon programming, but to **reclaim it with intention**. -Luca didn’t need to leave software behind—he needed to return to it **on his own terms**. - -Instead of chasing trends or reacting to deadlines, he built something that truly mattered. -The idea behind ImportSpy was simple, yet radical: - -**What if validation wasn’t just a chore? -What if it could be elegant, structural, and empowering?** - -That idea became the foundation of ImportSpy. -More than just a tool, it became a **manifesto**—a new way to bring structure to modular software -while rebuilding the joy of craftsmanship. - -More Than Code: A Community-Driven Journey ------------------------------------------- - -What began as a **personal experiment** quickly evolved into something greater. -ImportSpy isn’t just a validation framework—it’s a **testament to the resilience of developers** -who refuse to let their creativity be buried under routine. - -Luca’s path mirrors that of many developers trapped in roles that stifle innovation— -questioning their purpose in an industry that often rewards **velocity over clarity, -deadlines over design, and output over impact**. - -But ImportSpy offers another way. - -It’s a **reminder** that: - -- **Code should empower, not frustrate.** -- **Learning should be embraced—even when messy.** -- **Passion projects can reshape careers, communities, and industries.** - -ImportSpy represents a **new perspective**—one that values **structure, clarity, and the joy of building software that just works**. - -Why ImportSpy Matters ---------------------- - -ImportSpy isn’t just another dev tool—it’s a **statement**. - -- A statement that **modular software deserves structural validation and compliance by design**. -- A statement that **developers need tools that empower their workflows, not complicate them**. -- A statement that **side projects born from burnout can become catalysts for innovation**. - -Technically, ImportSpy ensures **stability, predictability, and architectural rigor** across your modules. -But its real power lies in its **philosophy**: -**build with purpose, validate with precision, and code with renewed passion**. - -Join the Movement ------------------ - -If you’ve ever felt **burned out, stuck, or disconnected** from your code, -know that you’re **not alone**. - -ImportSpy is more than a framework—it’s a **community-driven project** built on the belief that -software should be **precise, predictable, and a joy to create**. - -This project is a **tribute to all developers** reclaiming their craft and striving to build **better, smarter software**. -Because even the smallest ideas, when built with clarity, can have a **massive impact**. - -**🔹 Reclaim your passion. Build with confidence. Join the movement.** diff --git a/docs/source/overview/understanding_importspy/ci_cd_integration.rst b/docs/source/overview/understanding_importspy/ci_cd_integration.rst deleted file mode 100644 index 14fd2cd..0000000 --- a/docs/source/overview/understanding_importspy/ci_cd_integration.rst +++ /dev/null @@ -1,135 +0,0 @@ -CI/CD Integration -================= - -Modern CI/CD pipelines are powerful — but also fragile. - -Between dynamic environments, dependency drift, and plugin chaos, -it’s easy for code to pass local tests and **still fail at runtime**. - -ImportSpy brings a layer of **predictability, structural assurance**, and **contract enforcement** -to your automated workflows — making sure that every module behaves the way it should, -in the environment where it’s going to run. - -Why CI/CD Needs Structural Validation -------------------------------------- - -Functional tests catch **what your code does**. -ImportSpy ensures that it’s **running in the right place, with the right structure**. - -Without structural validation, pipelines are vulnerable to: - -- ❌ Hidden mismatches in **Python versions**, **interpreters**, or **platforms** -- ❌ Missing or malformed **environment variables** -- ❌ Untracked changes in shared modules or plugins -- ❌ Non-compliant third-party code with unexpected APIs - -These failures often appear **late**, when debugging is slow and costly. -ImportSpy helps you catch them **early**, at build time — not post-deploy. - -Where to Use ImportSpy in CI/CD -------------------------------- - -**1. During the Build Phase 🧱** - -Validate module structure before packaging: - -.. code-block:: bash - - importspy -s spymodel.yml mymodule.py - -This prevents broken contracts from ever making it into an artifact. - -**2. During Testing 🔬** - -Add ImportSpy validation before or alongside your unit tests: - -.. code-block:: bash - - importspy -s spymodel.yml -l ERROR path/to/module.py - -Catch unexpected mutations, missing methods, or API drift as part of CI. - -**3. Before Deployment 🚀** - -Use ImportSpy to verify: - -- Environment constraints (OS, Python, interpreter) -- Runtime assumptions (env vars, module-level variables) -- Plugin compliance across distributed services - -✅ If everything matches the contract, continue. -❌ If anything is wrong, block the deploy. - -Supported CI/CD Platforms --------------------------- - -ImportSpy is CI-native and works anywhere: - -- **GitHub Actions** - Add a step before your test matrix or deployment job. - -- **GitLab CI** - Use it in before_script or as a job stage. - -- **CircleCI / Jenkins** - Run via shell or Python-based jobs. - -- **Docker / Kubernetes** - Validate plugins or runtime images before deployment. - -- **Legacy or VM pipelines** - Enforce stability even in less dynamic stacks. - -Minimal Example for GitHub Actions ----------------------------------- - -.. code-block:: yaml - - - name: Validate Plugin - run: | - pip install importspy - importspy -s spymodel.yml extension.py - -Any contract violations will raise a `ValueError` and halt the build. - -Security Benefits ------------------- - -ImportSpy also strengthens your **software supply chain**: - -- Blocks unexpected or tampered code -- Prevents unauthorized plugin registration -- Confirms that runtime conditions are exactly what you expect -- Complements tools like `pip-audit`, `bandit`, or SAST engines - -Think of it as **import-time policy enforcement**, directly in your build. - -Best Practices for Integration ------------------------------- - -- 🔐 Treat ImportSpy as a **quality gate**, not just a linter -- 💥 Use `-l ERROR` log level to fail fast and get clear diagnostics -- 🔁 Keep contracts under version control with your code -- 🧪 Validate early, not just at release -- 🧭 Use strict contracts in production, relaxed ones in dev/test - -Related Topics --------------- - -- :doc:`contract_structure` – How to write import contracts -- :doc:`external_mode` – Running validation externally -- :doc:`spy_execution_flow` – See what happens during validation - -Summary -------- - -ImportSpy turns fragile CI pipelines into **predictable safety systems**. - -It guarantees that: - -- ✅ Every module is structurally sound -- ✅ Every environment matches your expectations -- ✅ Every build is trustworthy - -No more surprises. No more silent regressions. -Just clean, validated, future-proof Python — every time you deploy. diff --git a/docs/source/overview/understanding_importspy/contract_structure.rst b/docs/source/overview/understanding_importspy/contract_structure.rst deleted file mode 100644 index 3b47b8f..0000000 --- a/docs/source/overview/understanding_importspy/contract_structure.rst +++ /dev/null @@ -1,176 +0,0 @@ -Import Contract Structure -========================== - -Import contracts are the foundation of how ImportSpy performs validation. - -They are **YAML-based configuration files** that describe both the **structure** of a Python module and the **execution environments** in which it is valid. - -This page provides a deep dive into the schema, semantics, and flexibility of import contracts — and how they serve as **executable specifications** for modular systems. - -Overview --------- - -Each contract is made up of two primary blocks: - -- **Module definition**: describes what the module must contain (e.g., classes, functions, metadata) -- **Deployments**: lists the environments (OS, Python, interpreter) in which the module is allowed to run - -Contracts can define either: - -- **Global constraints**: structural requirements that apply to all deployments -- **Deployment-specific overrides**: context-sensitive rules based on platform or interpreter - -Top-Level Schema ----------------- - -Here is a reference of the main fields in a contract: - -Top-Level Fields -~~~~~~~~~~~~~~~~ - -- ``filename`` *(str)*: The name of the module to validate -- ``version`` *(str, optional)*: Expected module version (e.g., `__version__`) -- ``variables`` *(dict)*: Top-level variables and their expected values -- ``functions`` *(list)*: List of required functions -- ``classes`` *(list)*: List of required classes and their details -- ``deployments`` *(list)*: Permitted environments in which the module can be loaded - -Function Schema -~~~~~~~~~~~~~~~ - -Each function can define: - -- ``name``: Function name -- ``arguments``: List of parameter names and optional annotations -- ``return_annotation``: Optional return type annotation - -Class Schema -~~~~~~~~~~~~ - -Each class may include: - -- ``name``: Class name -- ``attributes``: - - ``type``: `"instance"` or `"class"` - - ``name``: Attribute name - - ``value``: Expected value - - ``annotation``: Optional type annotation -- ``methods``: Follows the same format as functions -- ``superclasses``: List of required base classes - -Deployments Block ------------------- - -The ``deployments`` section defines runtime compatibility requirements: - -- ``arch``: CPU architecture (e.g., `x86_64`, `arm64`) -- ``systems``: list of operating systems and environment constraints - - ``os``: `linux`, `windows`, or `macos` - - ``envs``: Required environment variables (`KEY: value`) - - ``pythons``: Supported Python versions and interpreters - - ``version``: Python version (e.g., `3.12.8`) - - ``interpreter``: `CPython`, `PyPy`, etc. - - ``modules``: Nested module definitions required in that context - -Global Module Definition (Baseline) ------------------------------------ - -If you define a module structure **outside the `deployments:` section**, -it acts as a **global baseline** — a structural requirement that applies to **all environments**. - -This section can include: - -- ``filename``, ``variables``, ``functions``, ``classes`` -- Shared interface contracts across platforms -- The minimum valid structure for the module to ever be imported - -.. note:: - This is semantically treated as a **lower bound**: - each deployment must satisfy the global structure plus any deployment-specific overrides. - -Full Example ------------- - -Here is a complete import contract: - -.. code-block:: yaml - - filename: extension.py - variables: - engine: docker - plugin_name: plugin name - plugin_description: plugin description - classes: - - name: Extension - attributes: - - type: instance - name: extension_instance_name - value: extension_instance_value - - type: class - name: extension_name - value: extension_value - methods: - - name: __init__ - arguments: - - name: self - - name: add_extension - arguments: - - name: self - - name: msg - annotation: str - return_annotation: str - - name: remove_extension - arguments: - - name: self - - name: http_get_request - arguments: - - name: self - superclasses: - - Plugin - - name: Foo - methods: - - name: get_bar - arguments: - - name: self - deployments: - - arch: x86_64 - systems: - - os: windows - pythons: - - version: 3.12.8 - interpreter: CPython - modules: - - filename: extension.py - variables: - author: Luca Atella - - version: 3.12.4 - modules: - - filename: addons.py - - interpreter: IronPython - modules: - - filename: addons.py - - os: linux - pythons: - - version: 3.12.8 - interpreter: CPython - modules: - - filename: extension.py - variables: - author: Luca Atella - -Validation Behavior --------------------- - -- All fields are **optional**, but the **hierarchy must be respected** -- Missing fields are simply skipped during validation -- Order of items in lists (methods, attributes) is **not enforced** -- Contracts are parsed into `SpyModel` objects during validation -- Validation is consistent across both embedded and CLI modes - -Related Topics --------------- - -- :doc:`defining_import_contracts` -- :doc:`spy_execution_flow` -- :doc:`embedded_mode` -- :doc:`external_mode` diff --git a/docs/source/overview/understanding_importspy/defining_import_contracts.rst b/docs/source/overview/understanding_importspy/defining_import_contracts.rst deleted file mode 100644 index 3c858e8..0000000 --- a/docs/source/overview/understanding_importspy/defining_import_contracts.rst +++ /dev/null @@ -1,156 +0,0 @@ -Defining Import Contracts -========================== - -Import contracts are at the core of how ImportSpy enforces structural and runtime compliance. - -They are **YAML-based declarations** that describe exactly how a Python module is expected to behave — not in terms of logic, but in terms of **structure, compatibility, and execution context**. - -This page explains how to define contracts, what they contain, and how ImportSpy uses them to validate modules at import time. - -What Are Import Contracts? --------------------------- - -An import contract tells ImportSpy: - -- 🔧 What a module **must contain** (functions, classes, attributes, variables) -- 🧠 Where it **is allowed to run** (Python version, OS, architecture, interpreter) -- 🔐 What **runtime conditions** must be met (environment variables, deployment setups) - -ImportSpy uses this contract to decide: -> “Should I allow this module to be used — or block it immediately?” - -Contracts are used in both validation modes: - -- **Embedded mode**: the validated module validates the importer at runtime -- **CLI mode**: a module is validated externally via `importspy -s contract.yml module.py` - -When to Use Them ----------------- - -Use import contracts when: - -- You need to **block incompatible modules** from being loaded -- You want to define **clear structure and interface expectations** -- Your code must **run only on certain platforms or interpreters** -- You’re building a system with **extensible plugins or dynamic modules** -- You need **environment-aware validation** during CI/CD or deployment - -Import Contract Anatomy ------------------------- - -Import contracts follow a hierarchical schema, with two main areas: - -Structure Requirements -~~~~~~~~~~~~~~~~~~~~~~ - -This defines what the module must expose: - -- `filename`: expected module file name -- `variables`: global-level constants or metadata -- `functions`: expected functions (with arguments and annotations) -- `classes`: expected classes, with methods, attributes, and superclasses - -Deployment Constraints -~~~~~~~~~~~~~~~~~~~~~~ - -This defines **where and under what conditions** the module is valid: - -- `arch`: expected CPU architecture (e.g., `x86_64`, `arm64`) -- `systems`: list of supported OS/platform combinations - - `os`: operating system (e.g., `linux`, `windows`) - - `envs`: required environment variables - - `pythons`: Python runtime environments - - `version`, `interpreter` (e.g., `CPython`, `PyPy`) - - `modules`: nested modules required in that Python env - -Contracts can declare **multiple deployments**, supporting flexibility while enforcing strict constraints. - -Baseline Constraints (Global Scope) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you define module-level fields **outside the `deployments:` section**, -they act as **baseline constraints** that apply to **all runtime environments**. - -This is useful when: - -- You want to enforce a shared structure across multiple deployments -- The module must always expose certain classes, functions, or variables -- You need a "lowest common denominator" for all environments - -.. note:: - These top-level rules define a **lower bound** for compliance — - each `deployment` can extend or specialize these expectations, - but never violate or ignore the top-level contract. - -Minimal Example ----------------- - -Here’s a basic contract example: - -.. code-block:: yaml - - filename: extension.py - variables: - plugin_name: core - classes: - - name: Extension - attributes: - - type: class - name: extension_name - value: core - methods: - - name: __init__ - arguments: - - name: self - - name: run - arguments: - - name: self - - name: data - annotation: str - return_annotation: str - deployments: - - arch: x86_64 - systems: - - os: linux - envs: - ENV: production - pythons: - - version: 3.12.8 - interpreter: CPython - modules: - - filename: extension.py - variables: - author: Luca Atella - -This contract: - -- Requires a class `Extension` with specific methods and attributes -- Enforces Linux OS, CPython 3.12.8, and a production environment -- Declares `extension.py` as the expected module file -- Requires a top-level variable `plugin_name` - -Design Principles ------------------- - -- Contracts are **declarative**: no logic, only structure and expectations -- All fields are optional — but if declared, they **must be met** -- Lists like `classes`, `methods`, and `attributes` are **order-independent** -- Contracts are parsed once per session for performance -- Contracts can express **baseline rules** (outside deployments) or per-environment logic - -Best Practices --------------- - -- Start minimal: validate structure first, then layer on environment constraints -- Version your contracts alongside your code — they are **enforceable documentation** -- Use deployments to support different runtime contexts while keeping control -- Always define `filename` — it's the root entry point for validation - -What’s Next? -------------- - -Now that you understand how to define contracts: - -- See how ImportSpy executes validation in :doc:`spy_execution_flow` -- Explore common validation patterns in the :doc:`validation_and_compliance` section -- Learn how contracts behave in :doc:`embedded_mode` and :doc:`external_mode` diff --git a/docs/source/overview/understanding_importspy/embedded_mode.rst b/docs/source/overview/understanding_importspy/embedded_mode.rst deleted file mode 100644 index a6c6801..0000000 --- a/docs/source/overview/understanding_importspy/embedded_mode.rst +++ /dev/null @@ -1,127 +0,0 @@ -Embedded Mode -============= - -Embedded mode allows a Python module to **protect itself** at runtime. - -Unlike external validation, where checks are triggered from outside, embedded mode runs ImportSpy **from within the module**, -verifying whether the environment that imported it complies with a declared contract. - -It’s a powerful mechanism to ensure that **your module only runs in safe, predictable, and validated contexts**. - -What Is Embedded Validation? ------------------------------ - -In embedded mode, the validated module: - -- ✅ Includes ImportSpy directly in its own code -- ✅ Loads a local `.yml` import contract (e.g., `spymodel.yml`) -- ✅ Introspects the caller (who is importing it) -- ✅ Validates the **importing environment**, not itself -- ❌ Refuses to execute if validation fails - -This is ideal for: - -- Plugins in plugin-based architectures -- Shared packages used across teams or platforms -- Sensitive modules with **runtime assumptions** (OS, interpreter, env vars) -- Security-hardened components - -How It Works ------------- - -Here’s the execution flow: - -1. 🧠 The module runs `Spy().importspy(...)` when imported -2. 📁 It parses its import contract (`spymodel.yml`) -3. 👀 It introspects the **importing module** (via stack trace) -4. 🔍 The importing context is matched against the contract: - - OS, CPU, Python version, interpreter - - Required env vars - - Module structure and metadata -5. ❌ If validation fails, a `ValueError` is raised and execution is blocked -6. ✅ If validation passes, the importing module is returned and can be used programmatically - -🔒 This creates a **Zero-Trust contract gate** — your module is only usable when the importing context is compliant. - -Example Usage --------------- - -Inside your protected module (e.g., `package.py`): - -.. code-block:: python - - from importspy import Spy - import logging - - caller_module = Spy().importspy(filepath="spymodel.yml", log_level=logging.DEBUG) - - # You now have access to the validated importer - caller_module.Foo().get_bar() - -Minimal Contract Example -------------------------- - -Here’s a simplified import contract for embedded validation: - -.. code-block:: yaml - - filename: extension.py - variables: - plugin_name: my_plugin - classes: - - name: Extension - methods: - - name: run - arguments: - - name: self - deployments: - - arch: x86_64 - systems: - - os: linux - pythons: - - version: 3.12.8 - interpreter: CPython - -This contract says: - -- Only modules named `extension.py` -- With a class `Extension` containing a `run(self)` method -- Are allowed to import this module -- Only on Linux + x86_64 + Python 3.12.8 + CPython - -If even one condition is not satisfied, execution is halted immediately. - -Why Use Embedded Mode? ------------------------ - -- ✅ The module validates **who is importing it** -- ✅ Ensures runtime safety without relying on external checks -- ✅ Makes plugins and extensions **self-defensive** -- ✅ Protects against unverified execution contexts in dynamic systems -- ✅ Integrates smoothly into plugin registries or dynamic loaders - -Best Practices --------------- - -- Always run embedded validation **at the top** of your module -- Version control both the module and its contract together -- Use detailed contracts in production, relaxed ones in dev/test -- Log validation steps using `log_level=logging.DEBUG` for traceability - -Comparison to External Mode ----------------------------- - -Use embedded mode when: - -- You want **tight control over where your module is used** -- You are building a **plugin** or **shared extension** -- You need to **validate the importing environment**, not just structure - -Use :doc:`external_mode` when you want to validate a module from the outside (e.g., in CI/CD). - -Related Topics --------------- - -- :doc:`contract_structure` – Learn how to define rich, nested import contracts -- :doc:`spy_execution_flow` – Understand how validation works under the hood -- :doc:`external_mode` – External validation for static and pipeline use cases diff --git a/docs/source/overview/understanding_importspy/error_handling.rst b/docs/source/overview/understanding_importspy/error_handling.rst deleted file mode 100644 index f06649b..0000000 --- a/docs/source/overview/understanding_importspy/error_handling.rst +++ /dev/null @@ -1,153 +0,0 @@ -Error Handling in ImportSpy -============================ - -Validation errors are not failures — they are **enforced expectations**. - -ImportSpy treats every contract violation as a **signal**, not just a disruption. -Its error system is designed to be **precise, informative, and traceable**, -helping developers identify and resolve problems early, consistently, and with confidence. - -Why Structured Errors Matter ----------------------------- - -In complex Python systems, especially those using plugins, microservices, or dynamic loading, -errors can be vague and hard to reproduce. - -ImportSpy solves this by generating: - -- 🧠 **Human-readable messages** with contextual hints -- 🧩 **Categorized errors**, sorted by validation layer -- 🛠️ **Diagnostic templates** that identify the cause and expected structure -- 🔎 **Traceable exceptions**, usable in CLI, IDE, or CI pipelines - -Whether you’re debugging a failing import or enforcing a strict policy in production, -ImportSpy makes validation feedback **clear, consistent, and useful**. - -Error Categories ------------------ - -ImportSpy groups errors into well-defined categories to simplify resolution: - -Missing Elements -~~~~~~~~~~~~~~~~ - -Raised when a required function, class, attribute, or variable is **not present**. - -Example: -`Missing method in class Extension: 'run'` - -Type or Value Mismatch -~~~~~~~~~~~~~~~~~~~~~~~ - -Triggered when types, annotations, or literal values do not match. - -Example: -`Return type mismatch: expected 'str', found 'None'` - -Environmental Misconfiguration -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Raised when runtime assumptions are unmet, such as: - -- Missing environment variables -- Incompatible OS or interpreter -- Python version mismatch - -Example: -`Missing required environment variable: API_KEY` - -Unsupported Runtime -~~~~~~~~~~~~~~~~~~~ - -Validation fails if the runtime environment does not match any declared `deployment`. - -Example: -`Unsupported Python version: 3.7. Expected: 3.12.8` - -The goal of these categories is to **pinpoint root causes** and prevent regression over time. - -Reference Error Table ----------------------- - -All known validation errors are defined in a centralized table: - -.. include:: error_table.rst - -Each entry includes: - -- A symbolic error constant (e.g., `Errors.CLASS_ATTRIBUTE_MISSING`) -- A dynamic message template -- A short description and suggested resolution - -These errors are **reused consistently across embedded and CLI validation**. - -Enforcement Strategies ------------------------ - -ImportSpy enforces contracts in strict mode by default: - -Strict Mode (Default) -~~~~~~~~~~~~~~~~~~~~~~ - -- ❌ Any error raises a `ValueError` -- ⛔ Execution halts immediately -- 🔐 Recommended for CI/CD, production, and regulated systems - -Soft Mode (Future Feature) -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -- ⚠️ Errors are downgraded to warnings -- 🔁 Execution proceeds -- 🧪 Ideal for development, onboarding, or exploratory validation - -Traceability and Debugging ---------------------------- - -Every raised exception includes: - -- The failing **element** (function, class, attribute, etc.) -- The **context** of the violation (e.g., deployment block or module scope) -- The **expected vs actual** values/types - -Thanks to integrated logging (`LogManager`) and specific exception classes (`ValidationError`, `PersistenceError`), -ImportSpy ensures traceability across: - -- Local debugging -- Containerized runtimes -- CI pipelines -- Logging dashboards - -Developer-Focused Feedback ---------------------------- - -Validation errors are formatted to be helpful across: - -- Terminal sessions and shell scripts -- IDE consoles with embedded validation -- CI output logs for quality gates or metrics - -If you're using ImportSpy in CLI mode, errors are printed with full detail — -ready to be parsed, logged, or even turned into automated reports. - -Best Practices --------------- - -- ✅ Run with `--log-level DEBUG` to get full trace on failure -- ✅ Keep `spymodel.yml` in version control and in sync with module changes -- ✅ Use error messages as checklists during onboarding or code review -- ✅ Integrate the error table into your internal docs or linting rules - -Errors Are Enforced Contracts ------------------------------- - -ImportSpy’s validation model is **contract-first** — if a rule is declared, it’s enforced. - -That means errors are not just problems — they’re confirmations that the system is working. -By treating every validation failure as a source of insight, ImportSpy helps your team: - -- Identify problems early -- Understand context clearly -- Move toward stronger modularity and runtime safety - -📌 Errors are not interruptions. -They are the **boundary where safety begins**. diff --git a/docs/source/overview/understanding_importspy/error_table.rst b/docs/source/overview/understanding_importspy/error_table.rst deleted file mode 100644 index 12f11e1..0000000 --- a/docs/source/overview/understanding_importspy/error_table.rst +++ /dev/null @@ -1,42 +0,0 @@ -.. list-table:: ImportSpy Validation Errors - :widths: 30 70 - :header-rows: 1 - - * - **Error Type** - - **Description** - * - `Missing Elements` - - A required **function**, **class**, **method**, or **attribute** is not found in the module or structure defined in the import contract. - * - `Type Mismatch` - - A return annotation, argument type, or class attribute type does **not match** the one declared in the contract. - * - `Value Mismatch` - - A variable or attribute exists but has a **different value** than expected (e.g., metadata mismatch). - * - `Function Argument Mismatch` - - A function's arguments do **not match in name, annotation, or default values**. - * - `Function Return Type Mismatch` - - The return type annotation of a function differs from the contract. - * - `Class Missing` - - A required class is **absent** from the module. - * - `Class Attribute Missing` - - One or more declared **class or instance attributes** are missing. - * - `Class Attribute Type Mismatch` - - A class attribute exists, but its **type or annotation** differs from what is expected. - * - `Superclass Mismatch` - - A class does not inherit from one or more required **superclasses** as declared. - * - `Variable Missing` - - A required **top-level variable** (e.g., `plugin_name`) is not defined in the module. - * - `Variable Value Mismatch` - - A variable exists but its value does not match the one declared in the contract. - * - `Filename Mismatch` - - The actual filename of the module differs from the one declared in `filename`. - * - `Version Mismatch` - - The module’s `__version__` (if defined) differs from the expected version. - * - `Unsupported Operating System` - - The current OS is **not included** in the allowed platforms (e.g., Linux, Windows, macOS). - * - `Missing Required Runtime` - - A required **architecture, OS, or interpreter version** is not satisfied. - * - `Unsupported Python Interpreter` - - The current interpreter (e.g., CPython, PyPy, IronPython) is not supported by the contract. - * - `Missing Environment Variable` - - A declared environment variable is **not present** in the current context. - * - `Invalid Environment Variable` - - An environment variable exists but contains an **unexpected value**. diff --git a/docs/source/overview/understanding_importspy/external_mode.rst b/docs/source/overview/understanding_importspy/external_mode.rst deleted file mode 100644 index 8aee7b5..0000000 --- a/docs/source/overview/understanding_importspy/external_mode.rst +++ /dev/null @@ -1,119 +0,0 @@ -External Mode -============= - -External mode allows you to use ImportSpy as a **standalone validator**, without embedding any logic in the module being validated. - -It’s ideal for teams who want to enforce structure and runtime compliance from the outside — -during **CI checks**, **code review gates**, or **manual inspections** of dynamic modules, plugins, or extensions. - -What Is External Validation? ----------------------------- - -In this mode, ImportSpy runs from the command line and: - -- Loads the target module dynamically -- Parses a separate **YAML import contract** -- Validates the module’s **structure**, **metadata**, and **runtime compatibility** -- Blocks execution if any contract rule is violated - -It’s perfect for use cases where **you don’t own the module**, or want to **validate before running anything at all**. - -Typical Use Cases ------------------- - -- ✅ Pre-deployment contract checks in CI/CD pipelines -- ✅ Validating plugins before registering them in a host application -- ✅ Enforcing runtime assumptions for sandboxed or remote code -- ✅ Auditing third-party extensions for structural and environmental compliance - -How to Use It -------------- - -1. Write your import contract (usually `spymodel.yml`): - -.. code-block:: yaml - - filename: extension.py - classes: - - name: Extension - methods: - - name: run - arguments: - - name: self - -2. Run the validation using the ImportSpy CLI: - -.. code-block:: bash - - importspy -s spymodel.yml -l DEBUG extension.py - -This will: - -- Load `extension.py` -- Parse `spymodel.yml` -- Validate all structure, types, env vars, OS, interpreter, and Python version -- Print any errors or mismatches to the terminal -- Exit with an error if validation fails - -Full CLI Reference -------------------- - -.. code-block:: text - - Usage: importspy [OPTIONS] [MODULEPATH] - - CLI command to validate a Python module against a YAML-defined import contract. - - Arguments: - modulepath Path to the Python module to load and validate. - - Options: - --version, -v Show ImportSpy version and exit. - --spymodel, -s TEXT Path to the import contract file (.yml). [default: spymodel.yml] - --log-level, -l [DEBUG|INFO|WARNING|ERROR] - Log level for output verbosity. - --install-completion Install completion for the current shell. - --show-completion Output shell snippet for autocompletion. - --help Show this message and exit. - -How External Validation Works 🔍 --------------------------------- - -Here’s what happens under the hood: - -1. 📥 **Contract is loaded** → Parsed from YAML into an internal `SpyModel` -2. 🧠 **Module is dynamically loaded** → No execution is triggered, just inspection -3. 🏗️ **Structure is reconstructed** → Classes, methods, attributes, annotations, etc. -4. 🌐 **Runtime context is gathered** → OS, architecture, interpreter, Python version, env vars -5. ⚖️ **Contract is evaluated** → Actual vs expected values are compared deeply -6. ❌ **Violations are raised** → A detailed `ValueError` is thrown with full diagnostics - -All of this happens **before any code is executed**, ensuring a safe, validated runtime context. - -Best Practices 🧪 ------------------ - -- Keep `.yml` contracts **under version control** -- Integrate into **CI/CD** to block broken modules from reaching production -- Use `--log-level DEBUG` to get full trace information when testing -- Validate all external plugins **before dynamic loading** -- Combine with :doc:`contract_structure` for clean, declarative specs - -Comparison to Embedded Mode ----------------------------- - -External mode: - -- ✅ Validates modules **without modifying them** -- ✅ Decouples validation logic from business logic -- ✅ Ideal for **automated pipelines** and **security reviews** - -If you want the **imported module to enforce rules about its importer**, -see :doc:`embedded_mode`. - -Related Topics --------------- - -- :doc:`contract_structure` – Full breakdown of contract syntax and nesting -- :doc:`spy_execution_flow` – Internals of validation lifecycle -- :doc:`embedded_mode` – For runtime protection from inside the validated module diff --git a/docs/source/overview/understanding_importspy/integration_best_practices.rst b/docs/source/overview/understanding_importspy/integration_best_practices.rst deleted file mode 100644 index ee975e9..0000000 --- a/docs/source/overview/understanding_importspy/integration_best_practices.rst +++ /dev/null @@ -1,144 +0,0 @@ -Integration Best Practices -=========================== - -ImportSpy is most powerful when it’s seamlessly integrated into your development lifecycle. -It acts as a **structural firewall** — ensuring that Python modules are only executed in validated environments, -with predictable interfaces and runtime guarantees. - -To get the most out of ImportSpy, it’s essential to follow practices that promote **clarity, maintainability**, -and long-term compliance. - -Contract Design Principles ---------------------------- - -A good import contract is: - -- 🧠 **Readable** → Easy to understand by developers and reviewers -- 🔁 **Reusable** → Avoids repetition by isolating shared environments -- 🔧 **Maintainable** → Easy to update as your system evolves -- 🎯 **Targeted** → Matches how your code is actually deployed, not just idealized setups - -Design your contracts with these goals in mind — and treat them as part of your project’s architecture. - -Modularize Your Contracts 🧱 ----------------------------- - -Avoid monolithic `.yml` files with everything mixed together. - -Instead: - -- Create **separate `deployments:` blocks** for each OS, architecture, or runtime -- Group constraints based on **real deployment contexts** (e.g., CI, Docker, staging) -- Keep **top-level structure global**, and specialize deeper in deployment-specific modules -- Use **baseline declarations** to enforce a minimum structure across all environments - -This makes your contracts scalable, and keeps them aligned with your actual execution model. - -Match Contracts to Real Environments ⚙️ ----------------------------------------- - -Don't write constraints that don't reflect reality. - -- ✅ In production: lock down OS, interpreter, variables, and structure -- ⚠️ In development: relax constraints slightly for flexibility -- 🧪 In CI: validate structure early, fail fast, and log everything - -ImportSpy’s power comes from accuracy — so your contract should describe **how your code really runs**, not how you wish it did. - -Reduce Duplication 🔁 ----------------------- - -Avoid repeating validation rules between modules. - -Strategies: - -- Define shared `deployments:` blocks and reuse them across multiple contracts -- Use generation tools to inject common blocks -- Extract reusable parts (e.g., shared classes or envs) into templated components - -Keeping contracts DRY improves maintainability and reduces the chance of silent mismatches. - -Structure Contracts Clearly 🏗️ -------------------------------- - -A recommended hierarchy: - -1. `filename`, `version`, and top-level `variables` -2. `functions` (optional, with argument specs) -3. `classes`, with: - - `attributes` (instance/class, with optional types and values) - - `methods`, defined like functions - - `superclasses` -4. `deployments`, each with: - - `arch`, `os`, and optional `envs` - - `pythons`, with version/interpreter/modules - - `modules`, repeating structure at the environment level if needed - -This structure allows deep validation of runtime-specific behavior. - -Validate Where It Matters 🌍 ----------------------------- - -ImportSpy is perfect for verifying: - -- Plugins or dynamic modules running in cloud or hybrid environments -- Modules that depend on env-specific config (e.g., secrets, endpoints) -- Microservices where drift between containers or hosts can break integrations - -Explicitly declare: - -- OS and architecture -- Required environment variables -- Supported Python versions and interpreters - -Consistency across environments starts with precise expectations. - -Embed ImportSpy in Your Pipeline 🧪 ------------------------------------ - -Use the CLI tool during CI builds and before deployments: - -.. code-block:: bash - - importspy -s spymodel.yml -l DEBUG path/to/module.py - -Fail the build if the contract isn't satisfied. -This catches integration issues **before** they reach staging or production. - -Consider combining ImportSpy with: - -- Linting (e.g., `ruff`, `flake8`) -- Typing (e.g., `mypy`) -- Unit and integration tests -- Security scanners - -ImportSpy adds **structural guarantees** on top of these tools. - -Choose Enforcement Mode Strategically 👥 ----------------------------------------- - -ImportSpy supports strict and soft enforcement: - -- **Strict Mode** → Violations raise `ValueError`. Use in CI and production. -- **Debug Logging** → Add `--log-level DEBUG` to trace without halting execution. -- **Soft Mode** (planned) → Logs validation failures as warnings. Ideal for onboarding or dry runs. - -Adapt validation levels to your team's tolerance for risk and your deployment maturity. - -Final Advice 🎯 ---------------- - -ImportSpy is not a replacement for testing — it complements it. - -It ensures your modules are used **only where and how they’re meant to be**, -preventing drift, mismatches, and unexpected runtime behavior. - -To integrate ImportSpy effectively: - -- 📁 Keep contracts clean and modular -- 🔄 Update them alongside the code they protect -- ⚙️ Match them to real-world runtimes -- 🚦 Automate validation in CI/CD -- 🔐 Use strict enforcement in trusted production pipelines - -ImportSpy helps you build **modular, secure, and future-proof Python systems** — one contract at a time. diff --git a/docs/source/overview/understanding_importspy/introduction.rst b/docs/source/overview/understanding_importspy/introduction.rst deleted file mode 100644 index 1b5dfab..0000000 --- a/docs/source/overview/understanding_importspy/introduction.rst +++ /dev/null @@ -1,113 +0,0 @@ -Introduction to ImportSpy -========================== - -Welcome to the core introduction to **ImportSpy** — -a validation and compliance framework that transforms Python's dynamic import system into a structured, predictable process. - -ImportSpy enables developers to define **executable contracts** that external modules must follow. -These contracts describe not only a module's expected structure, but also its **execution environment**, including: - -- Python version -- Interpreter type (e.g., CPython, PyPy, IronPython) -- Operating system -- Required classes, functions, variables -- Environment variables and metadata - -If the contract is not respected — the module doesn’t load. -It’s as simple and powerful as that. - -What Problem Does ImportSpy Solve? 🚧 -------------------------------------- - -Python is known for flexibility — but that comes at a cost: - -- Modules can silently drift from expected interfaces -- Plugin systems can misbehave if assumptions aren’t validated -- Deployment environments may differ in subtle, breaking ways -- Runtime errors often appear too late, and debugging them is slow and painful - -ImportSpy introduces **import-time validation**, enforcing that: - -✅ A module’s structure is as expected -✅ Its runtime context matches predefined constraints -✅ Violations are caught **before execution** begins - -The result? Safer systems, clearer boundaries, and predictable integrations. - -What Are Import Contracts? 📜 ------------------------------- - -Import contracts are YAML-based documents that define the rules a module must follow to be considered valid. - -They declare: - -- Required classes, methods, and attributes -- Module-level variables and metadata -- Expected Python interpreter, version, OS, and CPU architecture -- Environmental assumptions (e.g., required env vars) - -At runtime, ImportSpy parses these contracts and validates them **against the actual module and environment**. -These contracts serve as: - -- 🔍 Executable specifications -- 📖 Documentation for expected interfaces -- 🛡️ Runtime validation logic - -The result is a **formal, testable boundary** between modules — especially in dynamic systems like plugin frameworks. - -Validation Modes Supported 🔁 ------------------------------- - -ImportSpy supports two complementary modes of validation: - -.. list-table:: - :widths: 25 75 - :header-rows: 1 - - * - Mode - - Description - * - Embedded Mode - - The core module validates the structure of the **importer**. - Useful in plugin architectures where the base module ensures it is being imported in a safe, compliant context. - * - CLI Mode - - Validation is performed externally via the command line. - Ideal for pipelines, static checks, and CI/CD integration. - -This flexibility allows ImportSpy to adapt to **runtime and pre-deployment validation scenarios** with equal precision. - -What You’ll Learn in This Documentation 📘 ------------------------------------------- - -This documentation will guide you through: - -- 🚀 How ImportSpy works and why it matters -- 🛠️ How to define and apply import contracts -- 🔄 How validation is triggered in both modes -- 🧪 Real-world examples with plugins and pipelines -- ⚙️ Best practices for integration in production systems -- 🔐 Security benefits and enforcement patterns -- 💼 How to use ImportSpy in automated CI/CD workflows - -Installing ImportSpy ----------------------- - -To get started, install ImportSpy using pip: - -.. code-block:: bash - - pip install importspy - -Then visit: - -- :doc:`../../get_started/installation` to set up your environment -- :doc:`../../get_started/example_overview` to run your first validated example - -Let’s Get Started 🚀 ---------------------- - -ImportSpy turns Python’s imports into a **secure contract** — not just a hope for compatibility. - -By shifting validation **before execution**, it empowers developers to build modular, extensible, and production-safe Python systems. - -Ready to unlock the next level of confidence in your code? -Start by defining your first import contract and let ImportSpy take care of the rest. diff --git a/docs/source/overview/understanding_importspy/spy_execution_flow.rst b/docs/source/overview/understanding_importspy/spy_execution_flow.rst deleted file mode 100644 index 2e6cc94..0000000 --- a/docs/source/overview/understanding_importspy/spy_execution_flow.rst +++ /dev/null @@ -1,115 +0,0 @@ -Spy Execution Flow -=================== - -At the heart of ImportSpy lies a powerful validation engine that activates the moment an import occurs. - -Whether you're using **embedded mode** or **CLI validation**, ImportSpy reconstructs a full picture of the runtime environment, compares it to your declared import contract, and enforces compliance **before execution begins**. - -This page explains, step by step, how ImportSpy processes a validation request — from **introspection to enforcement**. - -Overview --------- - -ImportSpy enforces a simple but strict rule: - -> ❌ If the importing environment does not match the contract, the module is blocked. -> ✅ If the environment is compliant, execution proceeds normally. - -This model brings **predictability and control** to Python's otherwise dynamic import system. - -Execution Modes Supported --------------------------- - -ImportSpy works in two execution modes: - -- :doc:`Embedded Mode ` → The validated module runs `importspy()` internally to inspect its importer -- :doc:`External Mode ` → Validation is triggered via CLI, often in CI/CD or static validation steps - -Execution Pipeline ------------------- - -Here’s how ImportSpy validates imports, broken down into phases: - -1. Detect the Importer -~~~~~~~~~~~~~~~~~~~~~~ - -ImportSpy uses **stack introspection** to determine which module is importing the validated one. -This lets it establish a **validation boundary**, isolating the exact caller and its context. - -2. Capture Runtime Context -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Once the importer is found, ImportSpy collects runtime data: - -- Current OS and CPU architecture -- Python version and interpreter type -- Available environment variables -- Installed Python modules and paths - -This snapshot is encoded into an internal `SpyModel` object, representing the actual runtime conditions. - -3. Parse the Contract -~~~~~~~~~~~~~~~~~~~~~ - -Next, ImportSpy loads and parses the YAML-based import contract (typically `spymodel.yml`). - -This creates a second `SpyModel` representing the **expected structure and execution environment**. - -4. Compare & Validate -~~~~~~~~~~~~~~~~~~~~~ - -ImportSpy performs a deep comparison between the **actual SpyModel** and the **expected SpyModel**. - -Validation includes: - -- Matching CPU architecture and operating system -- Checking Python version and interpreter type -- Verifying required environment variables -- Matching declared functions, classes, methods, and attributes -- Validating module variables and custom metadata -- Checking nested modules and deployment-specific rules - -5. Enforce or Reject -~~~~~~~~~~~~~~~~~~~~~ - -- If the validation **fails**: - - ImportSpy raises a `ValueError` - - Detailed diagnostics are included in the exception - - Execution is halted immediately - -- If the validation **passes**: - - The validated importer is returned (in embedded mode) - - Execution proceeds safely - -6. Optimize for Runtime -~~~~~~~~~~~~~~~~~~~~~~~~ - -To avoid repeated validation during long-running processes or multi-import scenarios, ImportSpy uses **caching** to store validated environments. - -This provides fast re-entry for already-validated modules without compromising security. - -Security Philosophy -------------------- - -ImportSpy follows a **fail-fast and zero-trust model**: - -- 🚫 No module runs unless it satisfies all declared constraints -- ✅ All failures are explicit and traceable -- 🔐 This prevents silent regressions, broken interfaces, and unpredictable imports - -There is **no fallback behavior** — if a violation is detected, it is blocked **by design**. - -Best Practices --------------- - -- Use :doc:`embedded_mode` to validate who is importing your module -- Use :doc:`external_mode` to validate a module before deployment -- Always define structural + runtime constraints in your contract for full coverage - -Related Topics --------------- - -- :doc:`defining_import_contracts` -- :doc:`contract_structure` -- :doc:`error_handling` -- :doc:`validation_and_compliance` diff --git a/docs/source/overview/understanding_importspy/validation_and_compliance.rst b/docs/source/overview/understanding_importspy/validation_and_compliance.rst deleted file mode 100644 index 74a8f56..0000000 --- a/docs/source/overview/understanding_importspy/validation_and_compliance.rst +++ /dev/null @@ -1,129 +0,0 @@ -Validation and Compliance in ImportSpy -======================================= - -ImportSpy is more than a validator — it's a **compliance enforcement layer** -that ensures every import happens under the right conditions. - -It prevents fragile integrations, runtime surprises, and architectural drift by validating -**both the structure of modules** and **the environment they run in** — before they’re allowed to execute. - -Why Compliance Matters ------------------------ - -In modern Python systems, you often deal with: - -- Multiple operating systems and architectures -- Third-party plugins and dynamic module loading -- Varying Python runtimes across dev/staging/prod -- Environment variables that silently shape behavior - -Without strict validation, these differences introduce risk. - -ImportSpy guarantees that external modules only execute if their importing environment **matches the declared constraints** in their import contract. - -Multilayer Validation ----------------------- - -ImportSpy checks compliance at multiple levels: - -Execution Context -~~~~~~~~~~~~~~~~~ - -Before a module runs, ImportSpy validates: - -- Which module is trying to import it (via introspection) -- The **OS**, **architecture**, **Python version**, and **interpreter** -- Any required **environment variables** -- Whether the current runtime matches one of the allowed **deployment blocks** - -If anything is off, validation fails — no execution occurs. - -Example: - -- ✅ Declared: CPython 3.12.8 on Linux - ✅ Actual: CPython 3.12.8 on Linux → Allowed -- ❌ Declared: Windows-only - ❌ Actual: Linux → Rejected -- ❌ Declared: Requires `PLUGIN_KEY` - ❌ Missing from environment → Blocked - -Module Structure -~~~~~~~~~~~~~~~~ - -Structural validation includes: - -- Required **functions**, with specific argument names and annotations -- Required **classes**, including attributes, methods, and superclasses -- Top-level **variables** and their expected values -- Submodule definitions (when declared inside a `deployment`) - -If a module fails to meet these expectations, it's rejected at import time. - -System-Level Constraints -~~~~~~~~~~~~~~~~~~~~~~~~ - -ImportSpy allows contracts to specify: - -- Required environment variables -- Specific OS-level deployments -- Architecture-specific compatibility (e.g., only ARM64) - -This prevents issues like: - -- Silent misconfiguration -- Undeclared system assumptions -- Crashes due to missing runtime data - -Diagnostics and Failure Behavior --------------------------------- - -ImportSpy is strict: when validation fails, execution halts. - -But it also provides **high-quality error feedback**, including: - -- ❌ What went wrong (e.g., `"Missing method 'run'"`) -- ❌ Where the mismatch occurred (e.g., `"In class Extension"`) -- ❌ Why it’s invalid (e.g., `"Expected str, found int"`) - -Errors are raised as `ValueError` with clear stack traces and contract violation summaries. - -This fail-fast approach prevents issues from reaching production — or worse, going unnoticed. - -Maintaining Long-Term Compliance ---------------------------------- - -ImportSpy helps teams enforce long-term consistency via: - -- **Versioned contracts** that evolve with your code -- **Validation in CI/CD pipelines** to catch regressions early -- **Runtime checks** in production or sandbox environments - -This ensures: - -- No configuration drift -- No mismatches between staging and production -- Structural integrity across deployments and updates - -Compliance Is Not Optional ---------------------------- - -ImportSpy adopts a **Zero-Trust philosophy**: - -> ❌ No module is trusted without validation -> ✅ No import occurs unless the environment is approved - -This guarantees: - -- Secure plugin systems -- Stable microservice communication -- Predictable behavior across machines and versions - -If you care about runtime integrity, ImportSpy turns your import logic into **an enforceable contract** — and blocks anything that breaks it. - -Related Topics --------------- - -- :doc:`spy_execution_flow` -- :doc:`contract_structure` -- :doc:`error_handling` -- :doc:`integration_best_practices` diff --git a/docs/source/overview/understanding_importspy_index.rst b/docs/source/overview/understanding_importspy_index.rst deleted file mode 100644 index cdd1f9d..0000000 --- a/docs/source/overview/understanding_importspy_index.rst +++ /dev/null @@ -1,89 +0,0 @@ -Understanding ImportSpy 🔍 -========================== - -Welcome to the **technical heart** of ImportSpy. - -This section provides a full breakdown of how ImportSpy works, -why it matters in modern modular architectures, and how you can harness its full potential. - -ImportSpy isn’t just a utility — it’s a **runtime contract enforcement framework**. -It brings validation to the import system by ensuring that external modules conform to **predefined structural rules and runtime constraints**. -Whether used in embedded or CLI mode, ImportSpy guarantees **predictability, security, and compliance** before code ever runs. - -Why This Section Matters ⚠️ ----------------------------- - -Modern systems are complex. -You rely on dynamically loaded modules, plugins, APIs, microservices — often across multiple environments. - -But with this flexibility comes risk: - -- Missing or incompatible methods, attributes, or classes -- Subtle mismatches in Python versions, OS, or interpreters -- Unexpected behavior caused by configuration drift or structural divergence -- Modules that silently break contracts and cause late-stage failures - -ImportSpy intercepts these issues **before execution**, making validation **a first-class citizen** of your architecture. - -What You’ll Learn Here 📘 --------------------------- - -This section guides you through ImportSpy’s internals, starting from high-level concepts to runtime execution flow: - -- :doc:`understanding_importspy/introduction` - A clear introduction to ImportSpy’s core mission and use cases. - -- :doc:`understanding_importspy/defining_import_contracts` - Learn how to describe structural and environmental expectations through YAML contracts. - -- :doc:`understanding_importspy/contract_structure` - Understand the schema and semantics of a well-formed import contract. - -- :doc:`understanding_importspy/spy_execution_flow` - Discover how ImportSpy intercepts imports and validates modules in real time. - -- :doc:`understanding_importspy/embedded_mode` - Explore how modules can protect themselves by validating their importers at runtime. - -- :doc:`understanding_importspy/external_mode` - Understand CLI-based validation and its role in automation, CI/CD, and review pipelines. - -- :doc:`understanding_importspy/validation_and_compliance` - Dive into the validation engine and what it checks (types, names, annotations, OS, versions, etc). - -- :doc:`understanding_importspy/error_handling` - See how ImportSpy produces actionable, clear error messages when something doesn’t match. - -- :doc:`understanding_importspy/integration_best_practices` - Apply ImportSpy cleanly in real projects — plugins, microservices, libraries, or secure APIs. - -- :doc:`understanding_importspy/ci_cd_integration` - Integrate ImportSpy into your continuous delivery pipeline for automated contract enforcement. - -Who This Is For 👨‍💻👩‍💻 ---------------------------- - -This section is written for: - -- **Developers** using ImportSpy in plugin-based or multi-module Python apps -- **Architects** designing extensible, contract-driven software systems -- **DevOps and Security Engineers** aiming to validate runtime boundaries and block unknowns -- **Open Source Maintainers** who want to ensure compatibility across environments - -Ready to see how ImportSpy works under the hood? -Let’s explore the architecture that makes dynamic imports deterministic. - -.. toctree:: - :maxdepth: 2 - :caption: Understanding ImportSpy: - - understanding_importspy/introduction - understanding_importspy/defining_import_contracts - understanding_importspy/contract_structure - understanding_importspy/spy_execution_flow - understanding_importspy/embedded_mode - understanding_importspy/external_mode - understanding_importspy/validation_and_compliance - understanding_importspy/error_handling - understanding_importspy/integration_best_practices - understanding_importspy/ci_cd_integration diff --git a/docs/source/overview/use_cases/use_case_compilance.rst b/docs/source/overview/use_cases/use_case_compilance.rst deleted file mode 100644 index 05b77e9..0000000 --- a/docs/source/overview/use_cases/use_case_compilance.rst +++ /dev/null @@ -1,99 +0,0 @@ -Ensuring Compliance with Industry Standards -=========================================== - -📑 Enforcing Structural and Regulatory Conformance at the Module Level - -In industries governed by **strict regulations** — such as **finance**, **healthcare**, and **public sector platforms** — -software must not only function correctly, but also **prove compliance** with internal standards and external laws. - -Uncontrolled imports and unverified module interactions introduce risks that go far beyond bugs: - -- ⚠️ **Legal exposure**, from non-compliant code paths -- 🔓 **Security vulnerabilities**, exposing sensitive data -- 🐞 **Operational inconsistencies**, undermining traceability and auditability - -Modern teams need to **enforce validation before execution**, ensuring every module behaves exactly as expected -— across environments, platforms, and regulatory requirements. - -The Compliance Challenge -------------------------- - -A leading **healthcare SaaS provider** needed to secure their plugin system against non-compliant third-party modules. - -Their platform was required to uphold **HIPAA** and **GDPR** standards while supporting dynamic integration -with third-party services that could access **sensitive medical data**. - -Their core concerns: - -- 🔍 **Unstructured interactions** with plugin modules -- ❌ **No enforcement of structural expectations** -- 🕵️ **Poor audit visibility** over how and when validation occurred - -What they needed was **automated enforcement** at import time — a guardrail to **block non-compliant code -before it could execute**, with **evidence trails** for regulators and internal audits. - -How ImportSpy Solved It ------------------------- - -The team adopted **ImportSpy in CLI validation mode**, using **YAML-based import contracts** to: - -✅ **Define Compliance Constraints Declaratively** - -- Listed allowed **module names**, **functions**, **attributes**, and **expected annotations** -- Enforced execution limits: **Python version**, **OS**, **interpreter**, and **environment variables** -- Integrated contracts into the source repository as part of **version-controlled policy enforcement** - -✅ **Block Violations Before Runtime** - -- ImportSpy loaded the target module and validated it **against its declared contract** -- On mismatch, the module was **rejected immediately**, with detailed error feedback -- Violations raised `ValueError` exceptions, stopping non-compliant code from running - -✅ **Generate Audit Logs Automatically** - -- Each validation produced logs containing: - - Validation time and result - - Name of contract and validated module - - Structural mismatches or missing components -- Logs were parsed into **compliance dashboards** and shared with auditors - -Results: Measurable, Auditable Compliance ------------------------------------------ - -Before ImportSpy: - -- Compliance relied on **manual reviews and inconsistent scripts** -- Risk exposure was high due to **third-party code with unchecked access** -- Audits were painful, requiring **manual tracing of module usage across services** - -After ImportSpy: - -✅ All imported modules were validated automatically -✅ Violations were blocked before deployment or execution -✅ Audit trails were generated for every contract match/failure -✅ Compliance with **HIPAA**, **GDPR**, and internal policies was built into the lifecycle - -Why It Matters --------------- - -ImportSpy bridges the gap between **modular extensibility** and **regulatory control**. -By moving compliance checks to the import boundary, it ensures that **only verified, policy-compliant code** can run. - -Whether you're building regulated cloud platforms or securing internal APIs, ImportSpy gives your team: - -- ✅ **Automated, declarative validation** -- ✅ **Runtime protection against policy violations** -- ✅ **Structured logging for audits and security reviews** - -Try It Yourself ---------------- - -To validate a module before runtime, run: - -.. code-block:: bash - - importspy -s spymodel.yml your_module.py - -ImportSpy will block any import that doesn’t match the compliance contract — ensuring policy adherence before execution. - -📌 Import contracts are the new compliance policy — and ImportSpy is how you enforce them. diff --git a/docs/source/overview/use_cases/use_case_iot_integration.rst b/docs/source/overview/use_cases/use_case_iot_integration.rst deleted file mode 100644 index a16ed86..0000000 --- a/docs/source/overview/use_cases/use_case_iot_integration.rst +++ /dev/null @@ -1,112 +0,0 @@ -Ensuring Compliance in IoT Smart Home Integration -================================================= - -🔌 Real-World Enforcement Across Heterogeneous Devices - -In the evolving world of the **Internet of Things (IoT)**, ensuring **predictable behavior** across a wide variety of devices is no small feat. -Vendors, hardware platforms, Python runtimes, and execution environments all vary — making consistency difficult to guarantee. - -A leading IoT company, building a **smart home automation platform**, needed to support **third-party plugins** while maintaining **strict compliance**. -They required enforcement of **interface contracts**, **environmental conditions**, and **runtime compatibility** across a fragmented deployment landscape. - -📐 System Architecture ----------------------- - -The platform was designed around a **plugin-based architecture** that allowed modular integration of: - -- Smart thermostats -- Lighting controllers -- Security sensors -- Voice assistants -- External automation services - -Key architectural traits: - -- Device drivers implemented as Python plugins. -- Plugins communicate via a **RESTful API** layer. -- Deployed across edge devices like **Raspberry Pi**, as well as **Kubernetes-based smart hubs**. -- Environment setup includes: - - **ARM/x86_64** CPUs - - **Multiple Python versions** (3.10, 3.12) and **interpreters** (CPython, PyPy) - - **Dockerized plugins** with injected secrets via environment variables - -🧩 The Compliance Problem --------------------------- - -Without enforcement, plugins were deployed with: - -- Missing functions or improperly annotated interfaces -- Incorrect assumptions about Python version or CPU architecture -- Misconfigured or absent environment variables (`DEVICE_TOKEN`, `API_KEY`, etc.) -- Unvalidated structure that only failed **after** deployment - -These mismatches led to: - -- ❌ Runtime crashes in smart home hubs -- ❌ Inconsistent API behavior -- ❌ Security concerns due to environment misconfigurations -- ❌ Long debugging cycles and delayed releases - -🛡️ ImportSpy in Action: Embedded Validation Mode -------------------------------------------------- - -To regain control, the team embedded **ImportSpy** directly into each plugin’s entry point: - -.. code-block:: python - - from importspy import Spy - - caller_module = Spy().importspy(filepath="spymodel.yml") - -Plugins were paired with YAML-based **import contracts** that defined strict structural and runtime constraints. - -Contract enforcement ensured: - -✅ Structural Compliance - - Validated presence of all **required methods, attributes, and return types** - - Prevented silent schema drift between plugin and control layer - -✅ Runtime Compatibility - - Verified execution on the correct **CPU architecture** and **Python interpreter** - - Blocked execution on unsupported hardware setups - -✅ Environmental Validation - - Checked for required **env vars** (e.g., `DEVICE_TOKEN`, `PLATFORM_ENV`) - - Rejected execution if secrets were missing or malformed - -✅ Deployment Readiness - - CLI mode (`importspy -s spymodel.yml plugin.py`) used in **CI/CD pipelines** - - Pre-deployment validation embedded into Docker build stages - - Only validated containers promoted to **production clusters** - -🚀 Results in Production -------------------------- - -After adopting ImportSpy: - -- 🔒 **Plugin integrity was guaranteed pre-execution** -- 🐛 **Edge-device errors were eliminated before rollout** -- ⚙️ **CI pipelines caught contract violations early** -- 🔁 **New plugins were integrated 3× faster**, with fewer regressions - -Before ImportSpy: - -- Incompatible drivers were deployed to production -- Manual tests were required per device and platform -- Configuration bugs were discovered too late - -After ImportSpy: - -✅ Structural drift was eliminated -✅ Plugin execution was **bounded by contract** -✅ IoT integration became scalable and predictable - -Conclusion ----------- - -This real-world case shows how ImportSpy enables **modular safety** in highly distributed systems. -By turning contracts into enforcement mechanisms, it transforms each plugin into a **self-validating unit** — -capable of **refusing to run in invalid contexts**, and ensuring that integration is both safe and scalable. - -📦 ImportSpy is more than validation. -It’s **runtime insurance** for systems that must adapt — without compromising structure or control. diff --git a/docs/source/overview/use_cases/use_case_security.rst b/docs/source/overview/use_cases/use_case_security.rst deleted file mode 100644 index 76a2653..0000000 --- a/docs/source/overview/use_cases/use_case_security.rst +++ /dev/null @@ -1,120 +0,0 @@ -Strengthening Software Security with ImportSpy 🔐 -================================================= - -🔍 Enforcing Controlled Interactions with External Modules - -In security-critical software, **unregulated imports** are a gateway to vulnerabilities. -From misconfigured plugins to dynamic imports of malicious code, **Python’s flexibility becomes a liability** without structural enforcement. - -Organizations operating in fields like **cybersecurity**, **finance**, and **enterprise platforms** need more than just static analysis — -they need **runtime enforcement** that validates what is imported, how it behaves, and under which context it executes. - -🧨 The Problem: Invisible Risks in External Dependencies ---------------------------------------------------------- - -A cybersecurity firm specializing in **real-time threat detection** uncovered serious risks in its plugin framework: - -- Internal APIs were accessible via loosely defined module boundaries. -- External components bypassed authentication checks using dynamic imports. -- Function contracts were silently broken after dependency upgrades. -- No system-wide trace existed of who imported what — and under which conditions. - -These issues weren’t caused by malicious intent, but by the **absence of strict validation**. - -Without safeguards: - -- ⚠️ Plugins introduced **execution drift**. -- ⚠️ Imports became **non-deterministic** across environments. -- ⚠️ Attackers could **abuse loosely validated integrations**. - -🛡️ The Solution: ImportSpy Embedded + CLI Validation ------------------------------------------------------ - -The team introduced **ImportSpy** using both: - -- **Embedded Mode** for real-time validation at module import time. -- **CLI Mode** for enforcement in automated build pipelines. - -Each plugin and internal service was paired with a YAML-based **import contract** (`spymodel.yml`), defining strict: - -- Allowed functions and methods (including arguments and annotations) -- Required attributes and class hierarchies -- Valid operating systems, Python versions, and interpreters -- Mandatory environment variables for secrets or context - -📦 Example snippet from a contract: - -.. code-block:: yaml - - filename: secure_plugin.py - functions: - - name: verify_signature - arguments: - - name: data - annotation: bytes - return_annotation: bool - deployments: - - arch: x86_64 - systems: - - os: linux - envs: - SECURITY_TOKEN: required - pythons: - - version: 3.12.8 - interpreter: CPython - -⚙️ Security Mechanisms Enabled by ImportSpy --------------------------------------------- - -🔐 **Structural Boundary Enforcement (Embedded Mode)** - - ImportSpy executed *inside* secure modules to inspect who was importing them. - - If the importer didn’t match declared contracts, the execution was blocked. - - Validations were performed **every time the module was used**, ensuring active defense. - -🧪 **CI/CD Enforcement (CLI Mode)** - - ImportSpy was used in pipelines to validate plugins **before deployment**. - - Prevented misconfigured or unauthorized code from entering production. - - Ideal for automated checks on third-party or external codebases. - -🚫 **Blocking Unauthorized Imports** - - Attempted imports from unknown modules were rejected. - - Reflection-based imports (e.g. `importlib`, `__import__`) were intercepted if they bypassed structure. - -📈 **Audit-Ready Validation Logs** - - Each validation generated: - - Who imported the module and from where. - - Whether all structural, runtime, and environmental constraints were satisfied. - - A traceable record for security and compliance audits. - -🚀 Real Impact --------------- - -After adopting ImportSpy: - -✅ Only **pre-approved, contract-compliant modules** were allowed to interface with secure APIs -✅ All imports were **traceable and auditable**, including dynamic execution paths -✅ Teams could **prevent misuse of sensitive interfaces at runtime**, not just in reviews -✅ Security incidents related to uncontrolled plugin behavior dropped to zero - -Before ImportSpy: - -- Access to internal components was based on trust, not enforcement. -- Developers could unknowingly introduce insecure behaviors through third-party dependencies. -- Detection of misuses happened **after the fact**, during production failures or audits. - -After ImportSpy: - -✅ Security was enforced as **a contract**, not a convention -✅ Modules became **self-defensive**, refusing to run under unsafe conditions -✅ Compliance teams gained **real-time insight** into software integrity - -Conclusion ----------- - -ImportSpy transforms Python’s import mechanism into a **structural firewall**, -enforcing the principle of **Zero Trust by default**. - -Whether embedded in secure modules or integrated into CI/CD workflows, -it ensures that only **authorized, structurally sound, and contextually valid** code is ever executed. - -🔐 With ImportSpy, your code doesn’t just run — it runs **safely, predictably, and by the rules**. diff --git a/docs/source/overview/use_cases/use_case_validation.rst b/docs/source/overview/use_cases/use_case_validation.rst deleted file mode 100644 index bead272..0000000 --- a/docs/source/overview/use_cases/use_case_validation.rst +++ /dev/null @@ -1,99 +0,0 @@ -Validating Imports in Large-Scale Architectures -=============================================== - -🔍 Enforcing Predictable Module Integration Across Microservices - -In modern software platforms, especially those built around **microservices and shared components**, -imports can quickly become a **source of instability** if not explicitly controlled. - -ImportSpy addresses this challenge by enabling teams to define and enforce **import contracts**, -bringing structure, validation, and security to large-scale Python ecosystems. - -The Challenge: Structural Drift at Scale ----------------------------------------- - -A global fintech company operating a **real-time trading platform** faced a growing problem: - -- Over 200 services exchanged shared modules, but **no validation existed** on what those modules should look like. -- Developers introduced **untracked changes** to shared libraries — often without awareness of the ripple effect. -- Bugs emerged **during runtime**, causing unpredictable behavior in APIs, logs, and financial transactions. -- Regulatory audits revealed **unauthorized dependencies**, triggering compliance concerns. - -Without validation, **even a renamed method or removed class attribute** had the potential to break entire workflows -— often in systems critical to financial accuracy and regulatory visibility. - -How ImportSpy Resolved the Problem ----------------------------------- - -The team adopted ImportSpy to introduce **contract-based validation** between services. - -Each service defined a **`spymodel.yml`** contract that: - -- ✅ Declared which modules could be imported -- ✅ Specified required functions, classes, and their expected structure -- ✅ Described the allowed Python version, interpreter, and OS for each deployment context -- ✅ Enforced environmental assumptions like `env` variables and module metadata - -Validation was performed in two ways: - -- **Externally in CI/CD pipelines**, using the CLI tool -- **Dynamically at runtime**, via embedded validation inside critical modules - -Core Benefits for Large-Scale Systems --------------------------------------- - -🔒 **Structural Enforcement, Not Just Testing** - -Every import was validated against the contract: - -- Missing functions? ❌ Blocked -- Changed signatures? ❌ Blocked -- Incorrect return types? ❌ Blocked -- Drift in module metadata? ❌ Blocked - -🧩 **Modular Contracts per Microservice** - -Each team owned their own import contract, versioned alongside their codebase. -Contracts were reviewed in pull requests, giving visibility into integration assumptions. - -🛑 **Fail Fast, Fail Loud** - -When violations occurred, ImportSpy halted execution and raised detailed errors -before the application could misbehave. - -📋 **Compliance and Audit Alignment** - -Contracts became part of compliance reviews. -ImportSpy ensured that: - -- Only approved dependencies were used -- Environments matched what was declared -- Drift was caught before deployment - -🚀 Real-World Impact ---------------------- - -**Before ImportSpy**: - -- Services broke silently due to changing APIs -- Debugging required tracing through dozens of unrelated modules -- Compliance reports had no traceability on module-level expectations - -**After ImportSpy**: - -✅ Every shared module was paired with a structural contract -✅ Integration bugs were detected early in CI -✅ Teams had clear ownership and boundaries -✅ Compliance teams had visible, testable enforcement logic - -Conclusion ----------- - -ImportSpy enabled the company to treat imports as **governed integration points**, -not dynamic and unpredictable behaviors. - -It transformed their microservice architecture into a **contract-bound system**, -where validation was continuous, clear, and automated — at runtime and in pipelines. - -📌 For teams operating at scale, ImportSpy brings **structure, clarity, and runtime discipline** -to one of the most overlooked areas of Python: the import statement itself. diff --git a/docs/source/overview/use_cases_index.rst b/docs/source/overview/use_cases_index.rst deleted file mode 100644 index 37d4d91..0000000 --- a/docs/source/overview/use_cases_index.rst +++ /dev/null @@ -1,61 +0,0 @@ -Use Cases -========= - -🔍 Real-World Applications of ImportSpy - -ImportSpy is built for **real-world modular ecosystems** — where external components, plugins, and dynamic imports -must interact with precision, safety, and structural integrity. - -This section presents practical scenarios where ImportSpy ensures: - -- ✅ **Runtime validation** of external modules -- ✅ **Predictable integration** across complex environments -- ✅ **Compliance enforcement** in regulated and distributed systems - -Why Use Cases Matter --------------------- - -As modern applications adopt **microservices**, **plugin-based extensions**, and **cloud-native deployments**, -the complexity of imports grows — and so does the risk of: - -- ❌ Uncontrolled module behavior -- ❌ Silent contract violations -- ❌ Runtime incompatibilities across environments - -ImportSpy brings **order, visibility, and validation** to these architectures — blocking what doesn’t belong, and allowing only compliant modules to run. - -What You’ll Learn Here ------------------------ - -These case studies walk through: - -- **Modular IoT Environments** 🌐 - How ImportSpy validates plugins and services across **multi-device deployments** and **cross-platform runtimes**. - -- **Structural Validation in Dynamic Systems** 🧱 - Ensuring that plugins, modules, and APIs always match the expected **structure, behavior, and metadata**. - -- **Security and Runtime Threat Prevention** 🔒 - Protecting your application from **malicious imports**, **tampering**, and **unauthorized runtime mutations**. - -- **Regulatory Compliance and Policy Enforcement** 📋 - Using ImportSpy to meet **industry standards** by validating runtime environments, interpreters, and module structure. - -Use Case Preview ------------------ - -Each case is based on **realistic implementations**, designed to help teams: - -- Integrate ImportSpy into **CI/CD pipelines** -- Harden **plugin-based architectures** -- Secure **cloud, edge, and hybrid environments** -- Automate validation as part of **development workflows** - -.. toctree:: - :maxdepth: 2 - :caption: Use Cases - - use_cases/use_case_iot_integration - use_cases/use_case_validation - use_cases/use_case_security - use_cases/use_case_compilance diff --git a/docs/source/sponsorship.rst b/docs/source/sponsorship.rst deleted file mode 100644 index 00f97f6..0000000 --- a/docs/source/sponsorship.rst +++ /dev/null @@ -1,68 +0,0 @@ -Support the ImportSpy Mission 💡 -================================= - -ImportSpy is more than a validation tool — it’s a call for structure, precision, and integrity in the Python ecosystem. -By supporting ImportSpy, you’re backing a vision: one where Python modules behave as expected, where integrations are safe by design, and where developers can build **modular, reliable systems with confidence**. - -Whether you're an individual developer, an engineering team, or a tech leader, your support helps shape a future where **every Python import is secure, predictable, and compliant**. - -Why Your Support Matters 🚀 ----------------------------- - -ImportSpy is built on open-source values: **transparency, accessibility, and community-driven evolution**. -Your support enables us to: - -- **Accelerate Feature Development** 🛠️ - More funding means more focus. We can deliver powerful features, implement community requests, and evolve ImportSpy faster and more reliably. - -- **Expand Learning Resources** 📚 - Sponsor contributions help us invest in tutorials, video walkthroughs, onboarding guides, and examples that make ImportSpy accessible for everyone — from beginner to expert. - -- **Maintain Compatibility** 🔄 - Keeping up with the ever-changing Python ecosystem requires time and effort. Your support ensures ImportSpy stays compatible with new versions, platforms, and interpreters. - -- **Fuel Community Growth** 🌍 - From live workshops to online Q&As and collaborative initiatives — sponsors make it possible for us to build a **global, active, and inclusive developer community**. - -How You Can Help 💖 --------------------- - -**⭐ Star ImportSpy on GitHub** -Visibility matters. A single star tells the world this project matters. -`Star the project `_ - -**💝 Become a GitHub Sponsor** -Help us focus full-time on building and maintaining ImportSpy. -Your sponsorship directly funds the project's future. -`Sponsor ImportSpy `_ - -**📢 Spread the Word** -Tell a friend. Mention it in your team. Share it in your community. -Every conversation strengthens the ecosystem. - -**🔧 Contribute Code, Ideas, or Feedback** -Open issues, suggest features, or submit pull requests — your voice and your code matter. -We grow better with you involved. - -Thank You to Our Sponsors 💙 ------------------------------ - -Every sponsor — whether individual or organization — plays a vital role in ImportSpy’s growth. -We are deeply grateful for your belief in our mission and your commitment to building a more structured Python ecosystem. - -Your sponsorship supports: - -- Open development -- Better tools for the Python community -- A culture of care, rigor, and transparency - -Let’s Shape the Future Together 🔭 ------------------------------------ - -As a sponsor, you’re not just funding development — you’re helping define the roadmap. -We welcome your input on features, priorities, and directions for the future. - -ImportSpy exists because developers believed Python could be better. -With your help, we can **set a new standard for what “safe imports” look like.** - -**🔹 Support structure. Support clarity. Support ImportSpy.** diff --git a/docs/source/vision.rst b/docs/source/vision.rst deleted file mode 100644 index 21c2b8d..0000000 --- a/docs/source/vision.rst +++ /dev/null @@ -1,104 +0,0 @@ -The Vision Behind ImportSpy -============================ - -ImportSpy exists to solve a simple but powerful problem: -> How can we make dynamic Python imports **secure**, **predictable**, and **compliant**—without sacrificing flexibility? - -Modern Python development thrives on **modularity**, with architectures powered by **plugins, microservices, and third-party integrations**. -But the more dynamic our systems become, the harder it is to guarantee **structural consistency**, **environmental compatibility**, and **execution safety**. - -ImportSpy redefines how we think about `import` in Python. -It brings the **rigor of contracts** to the most permissive part of the language—validating structure, runtime context, and compliance **before code is allowed to execute**. - -What Problem Does ImportSpy Solve? ------------------------------------ - -Too often, developers rely on: - -- ✅ Best practices -- ✅ Static linters -- ✅ Runtime trial-and-error -- ✅ Outdated documentation - -…to ensure that external modules conform to expectations. But when things go wrong: - -- APIs silently drift -- Plugins break in production -- Modules fail across environments -- Security boundaries are crossed - -ImportSpy addresses these gaps **by enforcing executable contracts** at runtime—**automatically** and **contextually**. - -The Mission ------------ - -ImportSpy is designed to be the **compliance layer** for dynamic Python systems. - -Its mission is to: - -- 🧩 **Protect modular systems** from unpredictable imports -- 🔒 **Prevent integration errors** before they happen -- 🚦 **Validate structure, versioning, and runtime environment** -- 📜 **Promote living contracts** between modules and their runtime expectations - -And in doing so, it helps developers build systems that are: - -- Easier to maintain -- Safer to extend -- Ready for scale -- Aligned with compliance standards in regulated environments - -A Runtime Contract Philosophy ------------------------------- - -The vision behind ImportSpy is rooted in a new philosophy: - -> *“If a module must behave a certain way, let’s not hope it does — let’s validate it.”* - -By introducing **import contracts**, ImportSpy formalizes the structure of Python modules in YAML. -These contracts define what’s expected: -classes, methods, attributes, variables, Python versions, interpreters, OS targets, and more. - -At runtime, ImportSpy checks if those expectations are met — and if not, it blocks execution with **clear, actionable feedback**. - -Why This Matters ----------------- - -Today’s software is **distributed**, **heterogeneous**, and **highly modular**. -Whether you’re building for: - -- Embedded devices and IoT -- Plugin ecosystems -- Regulated sectors -- Containerized architectures -- Cloud-based platforms - -…you need to know that **the code running in production is exactly what you intended to deploy**. - -ImportSpy gives you that guarantee. - -It becomes a contract enforcer for: - -- **Security**: detect tampering and unauthorized changes -- **Compliance**: validate structural and environmental constraints -- **Stability**: prevent “it worked on my machine” failures -- **Clarity**: reduce guesswork and accelerate debugging - -Looking Ahead -------------- - -This is only the beginning. - -Future goals include: - -- ✨ Auto-generating contracts from Python modules -- 🔁 Bi-directional validation between contracts and code -- 🔍 Fine-grained integration with dependency graphs -- 🧠 Enhanced static-to-runtime consistency tooling -- 💼 First-class CI/CD and DevSecOps integrations - -With ImportSpy, we believe Python can be **both dynamic and dependable**. - -Join the movement toward **validated modularity**, and help shape a future where every import is safe, consistent, and predictable. - -**🔹 Structure with clarity. Import with confidence. Trust your code.** diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..c97182f --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1 @@ +site_name: My Docs diff --git a/poetry.lock b/poetry.lock index 4a46ad2..b6a158f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,15 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. - -[[package]] -name = "alabaster" -version = "1.0.0" -description = "A light, configurable Sphinx theme" -optional = false -python-versions = ">=3.10" -files = [ - {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, - {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, -] +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "annotated-types" @@ -17,165 +6,19 @@ version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] -[[package]] -name = "babel" -version = "2.17.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, -] - -[package.extras] -dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] - -[[package]] -name = "beautifulsoup4" -version = "4.13.4" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b"}, - {file = "beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195"}, -] - -[package.dependencies] -soupsieve = ">1.2" -typing-extensions = ">=4.0.0" - -[package.extras] -cchardet = ["cchardet"] -chardet = ["chardet"] -charset-normalizer = ["charset-normalizer"] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "certifi" -version = "2025.6.15" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -files = [ - {file = "certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057"}, - {file = "certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -files = [ - {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, - {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, - {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, - {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, - {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, - {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, - {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, - {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, - {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, - {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, -] - [[package]] name = "click" version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -190,21 +33,12 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] - -[[package]] -name = "docutils" -version = "0.21.2" -description = "Docutils -- Python Documentation Utilities" -optional = false -python-versions = ">=3.9" -files = [ - {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, - {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, -] +markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} [[package]] name = "exceptiongroup" @@ -212,6 +46,8 @@ version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, @@ -224,46 +60,22 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} test = ["pytest (>=6)"] [[package]] -name = "furo" -version = "2024.8.6" -description = "A clean customisable Sphinx documentation theme." +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." optional = false -python-versions = ">=3.8" +python-versions = "*" +groups = ["main"] files = [ - {file = "furo-2024.8.6-py3-none-any.whl", hash = "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c"}, - {file = "furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01"}, + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, ] [package.dependencies] -beautifulsoup4 = "*" -pygments = ">=2.7" -sphinx = ">=6.0,<9.0" -sphinx-basic-ng = ">=1.0.0.beta2" - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] +python-dateutil = ">=2.8.1" [package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] +dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "iniconfig" @@ -271,6 +83,7 @@ version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, @@ -282,6 +95,7 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -293,12 +107,29 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "markdown" +version = "3.8.2" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24"}, + {file = "markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45"}, +] + +[package.extras] +docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + [[package]] name = "markdown-it-py" version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -323,6 +154,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -393,28 +225,120 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + [[package]] name = "packaging" version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + [[package]] name = "pluggy" version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -430,6 +354,7 @@ version = "2.11.7" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, @@ -443,7 +368,7 @@ typing-inspection = ">=0.4.0" [package.extras] email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] [[package]] name = "pydantic-core" @@ -451,6 +376,7 @@ version = "2.33.2" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, @@ -562,6 +488,7 @@ version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["main", "dev"] files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, @@ -576,6 +503,7 @@ version = "8.4.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, @@ -594,25 +522,97 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] -name = "requests" -version = "2.32.4" -description = "Python HTTP for Humans." +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" optional = false -python-versions = ">=3.8" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ - {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, - {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" +six = ">=1.5" -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +description = "A custom YAML tag for referencing environment variables in YAML files." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"}, + {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"}, +] + +[package.dependencies] +pyyaml = "*" [[package]] name = "rich" @@ -620,6 +620,7 @@ version = "14.0.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" +groups = ["main"] files = [ {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, @@ -639,6 +640,7 @@ version = "0.18.14" description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "ruamel.yaml-0.18.14-py3-none-any.whl", hash = "sha256:710ff198bb53da66718c7db27eec4fbcc9aa6ca7204e4c1df2f282b6fe5eb6b2"}, {file = "ruamel.yaml-0.18.14.tar.gz", hash = "sha256:7227b76aaec364df15936730efbf7d72b30c0b79b1d578bbb8e3dcb2d81f52b7"}, @@ -657,6 +659,8 @@ version = "0.2.12" description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" optional = false python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\" and python_version < \"3.14\"" files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, @@ -712,205 +716,32 @@ version = "1.5.4" description = "Tool to Detect Surrounding Shell" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] [[package]] -name = "snowballstemmer" -version = "3.0.1" -description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] files = [ - {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, - {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] -[[package]] -name = "soupsieve" -version = "2.7" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = false -python-versions = ">=3.8" -files = [ - {file = "soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4"}, - {file = "soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a"}, -] - -[[package]] -name = "sphinx" -version = "8.1.3" -description = "Python documentation generator" -optional = false -python-versions = ">=3.10" -files = [ - {file = "sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2"}, - {file = "sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927"}, -] - -[package.dependencies] -alabaster = ">=0.7.14" -babel = ">=2.13" -colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} -docutils = ">=0.20,<0.22" -imagesize = ">=1.3" -Jinja2 = ">=3.1" -packaging = ">=23.0" -Pygments = ">=2.17" -requests = ">=2.30.0" -snowballstemmer = ">=2.2" -sphinxcontrib-applehelp = ">=1.0.7" -sphinxcontrib-devhelp = ">=1.0.6" -sphinxcontrib-htmlhelp = ">=2.0.6" -sphinxcontrib-jsmath = ">=1.0.1" -sphinxcontrib-qthelp = ">=1.0.6" -sphinxcontrib-serializinghtml = ">=1.1.9" -tomli = {version = ">=2", markers = "python_version < \"3.11\""} - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=6.0)", "mypy (==1.11.1)", "pyright (==1.1.384)", "pytest (>=6.0)", "ruff (==0.6.9)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241005)", "types-requests (==2.32.0.20240914)", "types-urllib3 (==1.26.25.14)"] -test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] - -[[package]] -name = "sphinx-basic-ng" -version = "1.0.0b2" -description = "A modern skeleton for Sphinx themes." -optional = false -python-versions = ">=3.7" -files = [ - {file = "sphinx_basic_ng-1.0.0b2-py3-none-any.whl", hash = "sha256:eb09aedbabfb650607e9b4b68c9d240b90b1e1be221d6ad71d61c52e29f7932b"}, - {file = "sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9"}, -] - -[package.dependencies] -sphinx = ">=4.0" - -[package.extras] -docs = ["furo", "ipython", "myst-parser", "sphinx-copybutton", "sphinx-inline-tabs"] - -[[package]] -name = "sphinx-tabs" -version = "3.4.7" -description = "Tabbed views for Sphinx" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sphinx-tabs-3.4.7.tar.gz", hash = "sha256:991ad4a424ff54119799ba1491701aa8130dd43509474aef45a81c42d889784d"}, - {file = "sphinx_tabs-3.4.7-py3-none-any.whl", hash = "sha256:c12d7a36fd413b369e9e9967a0a4015781b71a9c393575419834f19204bd1915"}, -] - -[package.dependencies] -docutils = "*" -pygments = "*" -sphinx = ">=1.8" - -[package.extras] -code-style = ["pre-commit (==2.13.0)"] -testing = ["bs4", "coverage", "pygments", "pytest (>=7.1,<8)", "pytest-cov", "pytest-regressions", "rinohtype"] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, - {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, - {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, - {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["html5lib", "pytest"] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -optional = false -python-versions = ">=3.5" -files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] - -[package.extras] -test = ["flake8", "mypy", "pytest"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, - {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["defusedxml (>=0.7.1)", "pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" -optional = false -python-versions = ">=3.9" -files = [ - {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, - {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - [[package]] name = "tomli" version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -952,6 +783,7 @@ version = "0.15.4" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "typer-0.15.4-py3-none-any.whl", hash = "sha256:eb0651654dcdea706780c466cf06d8f174405a659ffff8f163cfbfee98c0e173"}, {file = "typer-0.15.4.tar.gz", hash = "sha256:89507b104f9b6a0730354f27c39fae5b63ccd0c95b1ce1f1a6ba0cfd329997c3"}, @@ -969,10 +801,12 @@ version = "4.14.1" description = "Backported and Experimental Type Hints for Python 3.9+" optional = false python-versions = ">=3.9" +groups = ["main", "dev"] files = [ {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, ] +markers = {dev = "python_version == \"3.10\""} [[package]] name = "typing-inspection" @@ -980,6 +814,7 @@ version = "0.4.1" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, @@ -989,23 +824,49 @@ files = [ typing-extensions = ">=4.12.0" [[package]] -name = "urllib3" -version = "2.5.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." +name = "watchdog" +version = "6.0.0" +description = "Filesystem events monitoring" optional = false python-versions = ">=3.9" -files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +groups = ["main"] +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +watchmedo = ["PyYAML (>=3.10)"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.10" -content-hash = "4821238bf1d24a0a5c5afd1d7852fd0862b7e2dfaf564a897df87c8e1310107b" +content-hash = "550dcabca1cba56ff54d8fc937ed16b4b328554be192a8641a4ac544985713bb" diff --git a/pyproject.toml b/pyproject.toml index 8b8e26a..5b4618d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,13 +12,11 @@ python = "^3.10" pydantic = "^2.9.2" ruamel-yaml = "^0.18.10" typer = "^0.15.2" +mkdocs = "^1.6.1" [tool.poetry.group.dev.dependencies] -furo = "^2024.8.6" -sphinx = ">=5,<9" pytest = "^8.3.3" -sphinx-tabs = "^3.4.7" [tool.poetry.urls] Repository = "https://github.com/atellaluca/importspy" From 1184328e89b78f24b0ad3fd88aa3101f4d9db95b Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Thu, 31 Jul 2025 17:48:07 +0200 Subject: [PATCH 29/40] build(docs): configure MkDocs site metadata and theme --- docs/api-reference.md | 0 docs/contracts/syntax.md | 0 docs/contribute.md | 0 docs/errors/contract_violations.md | 24 +++ docs/errors/error_table.md | 10 + docs/intro/install.md | 46 +++++ docs/intro/overview.md | 74 ++++++++ docs/intro/quickstart.md | 98 ++++++++++ docs/modes/cli.md | 282 +++++++++++++++++++++++++++++ docs/modes/embedded.md | 75 ++++++++ docs/use_cases/index.md | 45 +++++ mkdocs.yml | 54 +++++- poetry.lock | 21 ++- pyproject.toml | 1 + 14 files changed, 728 insertions(+), 2 deletions(-) create mode 100644 docs/api-reference.md create mode 100644 docs/contracts/syntax.md create mode 100644 docs/contribute.md create mode 100644 docs/errors/contract_violations.md create mode 100644 docs/errors/error_table.md create mode 100644 docs/intro/install.md create mode 100644 docs/intro/overview.md create mode 100644 docs/intro/quickstart.md create mode 100644 docs/modes/cli.md create mode 100644 docs/modes/embedded.md create mode 100644 docs/use_cases/index.md diff --git a/docs/api-reference.md b/docs/api-reference.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/contracts/syntax.md b/docs/contracts/syntax.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/contribute.md b/docs/contribute.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/errors/contract_violations.md b/docs/errors/contract_violations.md new file mode 100644 index 0000000..4eec19f --- /dev/null +++ b/docs/errors/contract_violations.md @@ -0,0 +1,24 @@ +# Contract Violations + +ImportSpy raises clear and structured errors when an import contract is not satisfied. + +Each error message includes: +- The **context** of the violation (runtime, environment, module, class) +- The **type** of error (missing, mismatch, invalid) +- A **precise description** of the problem and how to fix it + +Below is a list of possible contract violation messages you may encounter. + +--- + +## 📋 Error Messages + +--8<-- "errors/error_table.md" + +--- + +## 🔗 Related Topics + +- [Contract Syntax](../contracts/syntax.md) +- [Embedded Mode](../modes/embedded.md) +- [CLI Mode](../modes/cli.md) diff --git a/docs/errors/error_table.md b/docs/errors/error_table.md new file mode 100644 index 0000000..0760d81 --- /dev/null +++ b/docs/errors/error_table.md @@ -0,0 +1,10 @@ +| Category | Context | Error Message | +|------------|---------------|---------------| +| `missing` | `runtime` | The runtime `CPython 3.12` is declared but missing. Ensure it is properly defined and implemented. | +| | `environment` | The environment variable `DEBUG` is declared but missing. Ensure it is properly defined and implemented. | +| | `module` | The variable `plugin_name` in module `extension.py` is declared but missing. Ensure it is properly defined and implemented. | +| | `class` | The method `run` in class `Plugin` is declared but missing. Ensure it is properly defined and implemented. | +| `mismatch` | `runtime` | The runtime `CPython 3.12` does not match the expected value. Expected: `CPython 3.11`, Found: `CPython 3.12`. Check the value and update the contract or implementation accordingly. | +| | `environment` | The environment variable `LOG_LEVEL` does not match the expected value. Expected: `'INFO'`, Found: `'DEBUG'`. Check the value and update the contract or implementation accordingly. | +| | `class` | The class attribute `engine` in class `Extension` does not match the expected value. Expected: `'docker'`, Found: `'podman'`. Check the value and update the contract or implementation accordingly. | +| `invalid` | `class` | The argument `msg` of method `send` has an invalid value. Allowed values: `[str, None]`, Found: `42`. Update the value to one of the allowed options. | diff --git a/docs/intro/install.md b/docs/intro/install.md new file mode 100644 index 0000000..0b9e24a --- /dev/null +++ b/docs/intro/install.md @@ -0,0 +1,46 @@ +# Installation + +You can install ImportSpy directly from [PyPI](https://pypi.org/project/importspy/) using `pip`. + +--- + +## Basic installation + +```bash +pip install importspy +``` + +This installs both the core runtime and the command-line interface (CLI), allowing you to use ImportSpy in both **Embedded Mode** and **CLI Mode**. + +--- + +## Minimum requirements + +- **Python 3.8 or higher** +- Supported operating systems: + - Linux + - macOS + - Windows +- Compatible with CPython and alternative interpreters (e.g. IronPython), if declared in the contract + +--- + +## Updating ImportSpy + +To upgrade to the latest version: + +```bash +pip install --upgrade importspy +``` + +--- + +## Verify installation + +To confirm that ImportSpy is correctly installed and the CLI is available: + +```bash +importspy --help +``` + +You should see the full list of command-line options and usage instructions. diff --git a/docs/intro/overview.md b/docs/intro/overview.md new file mode 100644 index 0000000..a9f8892 --- /dev/null +++ b/docs/intro/overview.md @@ -0,0 +1,74 @@ +# What is ImportSpy? + +**ImportSpy** is a Python library that brings context awareness to the most fragile point in the lifecycle of a module: its import. + +It lets developers declare explicit import contracts — versionable `.yml` files that describe under which conditions a module can be imported. These contracts are validated at runtime, ensuring that the importing environment (and optionally the importing module) matches the declared requirements. + +If the conditions are not satisfied, the import fails immediately, with a detailed and structured error message. + +--- + +## Why use ImportSpy? + +Python offers no built-in mechanism to control how and where a module can be imported. In modular systems, plugin frameworks, and regulated environments, this leads to: + +- Unexpected runtime errors +- Hard-to-diagnose misconfigurations +- Fragile CI/CD workflows + +ImportSpy solves this by introducing **import-time validation** based on: + +- Runtime environment (OS, CPU, Python version, interpreter) +- Required environment variables and secret presence +- Structural expectations of the importing module (classes, methods, types…) + +This results in safer, more predictable imports — whether you're building a plugin system, enforcing architectural rules, or protecting critical components. + +--- + +## How does it work? + +ImportSpy uses a **declarative import contract** — a `.yml` file written by the developer — to define expected conditions. + +At runtime, this contract is parsed into a structured Python object called a **SpyModel**, which is used internally for validation. + +Depending on the operation mode (Embedded or CLI), ImportSpy: + +- Validates the runtime and system environment +- Optionally inspects the module that is importing the protected one +- Enforces declared constraints on structure, type annotations, variable values, and more + +If all constraints are respected, the import succeeds. +If not, a descriptive error is raised (e.g. `ValueError`, `ImportSpyViolation`). + +--- + +## Key features + +- ✅ Declarative, versionable `.yml` import contracts +- 🧠 Runtime validation of OS, CPU architecture, Python version/interpreter +- 🔍 Structural checks on the importing module (classes, methods, attributes, types) +- 🔐 Validation of required environment variables and secrets (presence only) +- 🚀 Dual operation modes: Embedded Mode and CLI Mode +- 🧪 Full integration with CI/CD pipelines +- 📋 Structured, human-readable error reports with actionable messages + +--- + +## Who is it for? + +ImportSpy is designed for: + +- Plugin frameworks that enforce structural compliance +- Systems with strict version, runtime or security constraints +- Teams building modular Python applications +- Projects that need to validate import-time compatibility +- Open-source maintainers looking to define and enforce import boundaries + +--- + +## What’s next? + +- [→ Install ImportSpy](install.md) +- [→ Try a minimal working example](quickstart.md) +- [→ Learn about the operation modes](../modes/embedded.md) diff --git a/docs/intro/quickstart.md b/docs/intro/quickstart.md new file mode 100644 index 0000000..93c984f --- /dev/null +++ b/docs/intro/quickstart.md @@ -0,0 +1,98 @@ +# Quickstart + +This quickstart shows how to use **ImportSpy in Embedded Mode** to protect a Python module from being imported in an invalid context. + +--- + +## Step 1 — Install ImportSpy + +If you haven’t already: + +```bash +pip install importspy +``` + +--- + +## Step 2 — Create a contract (`spymodel.yml`) + +This file defines the conditions under which your module can be imported. +For example, it can require specific Python versions, operating systems, or structure in the calling module. + +```yaml +filename: plugin.py +classes: + - name: Plugin + methods: + - name: run + arguments: + - name: self + return_annotation: +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.12 + interpreter: CPython +``` + +Save this file as `spymodel.yml`. + +--- + +## Step 3 — Protect your module + +Here’s how to use ImportSpy inside the module you want to protect (e.g. `plugin.py`): + +```python +# plugin.py +from importspy import Spy + +caller = Spy().importspy(filepath="spymodel.yml") + +# Call something from the importer (for example) +caller.MyPlugin().run() +``` + +This checks the current environment and the module that is importing `plugin.py`. +If it doesn’t match the contract, ImportSpy raises an error and blocks the import. + +--- + +## Step 4 — Create an importer + +Write a simple module that tries to import `plugin.py`. + +```python +# main.py +class MyPlugin: + def run(self): + print("Plugin running") + +import plugin +``` + +--- + +## Step 5 — Run it + +If the environment and structure of `main.py` match the contract, the import will succeed: + +```bash +python main.py +``` + +Otherwise, you'll get a clear and structured error like: + +``` +[Structure Violation] Missing required class 'Plugin' in caller module. +``` + +--- + +## Next steps + +- Learn more about [Embedded Mode](../modes/embedded.md) +- Explore [CLI Mode](../modes/cli.md) for validating modules from the outside +- Dive into [contract syntax](../contracts/syntax.md) to write more advanced rules diff --git a/docs/modes/cli.md b/docs/modes/cli.md new file mode 100644 index 0000000..254c3f5 --- /dev/null +++ b/docs/modes/cli.md @@ -0,0 +1,282 @@ +# CLI Mode + +ImportSpy can also be used **outside of runtime** to validate a Python module against a contract from the command line. + +This is useful in **CI/CD pipelines**, **pre-commit hooks**, or manual validations — whenever you want to enforce import contracts without modifying the target module. + +--- + +## How it works + +In CLI Mode, you invoke the `importspy` command and provide: + +- The path to the **module** to validate +- The path to the **YAML contract** +- (Optional) a log level for output verbosity + +ImportSpy loads the module dynamically, builds its SpyModel, and compares it against the `.yml` contract. + +If the module is non-compliant, the command will: + +- Exit with a non-zero status +- Print a structured error explaining the violation + +--- + +## Basic usage + +```bash +importspy extensions.py -s spymodel.yml -l WARNING +``` + +### CLI options + ++-----------------------------------------------------------------------------+ +| Flag | Description | +|--------------------|--------------------------------------------------------| +| `-s, --spymodel` | Path to the import contract `.yml` file | +| `-l, --log-level` | Logging verbosity: `DEBUG`, `INFO`, `WARNING`, `ERROR` | +| `-v, --version` | Show ImportSpy version | +| `--help` | Show help message and exit | ++-----------------------------------------------------------------------------+ +--- + +## Example project + +Let’s look at a full CLI-mode validation example. + +### Project structure + +``` +pipeline_validation/ +├── extensions.py +└── spymodel.yml +``` + +### 📄 Source files + +=== "extensions.py" + +```python +--8<-- "examples/plugin_based_architecture/pipeline_validation/extensions.py" +``` + +=== "spymodel.yml" + +```yaml +--8<-- "examples/plugin_based_architecture/pipeline_validation/spymodel.yml" +``` + +### 🔍 Run validation + +```bash +cd examples/plugin_based_architecture/pipeline_validation +importspy extensions.py -s spymodel.yml -l WARNING +``` + +If the module matches the contract, the command exits silently with `0`. +If it doesn't, you’ll see a structured error like: + +``` +[Structure Violation] Missing required method 'get_bar' in class 'Foo'. +``` + +--- + +## When to use CLI Mode + +!!! tip "Use CLI Mode for automation" + CLI Mode is ideal when you want to: + + - Validate modules **without changing their code** + - Integrate checks in **CI/CD pipelines** + - Enforce contracts in **external packages** + - Run **batch validations** over multiple files + +--- + +## Learn more + +- [→ Embedded Mode](embedded.md) +- [→ Contract syntax](../contracts/syntax.md) +- [→ Error types](../errors/index.md) +# Import Contract Syntax + +An ImportSpy contract is a YAML file that describes: + +- The **structure** expected in the calling module (classes, methods, variables…) +- The **runtime and system environment** where the module is allowed to run +- The required **environment variables** and optional secrets + +This contract is parsed into a `SpyModel`, which is then compared against the actual runtime and importing module. + +--- + +## ✅ Overview + +Here’s a minimal but complete contract: + +```yaml +filename: extension.py +variables: + - name: engine + value: docker +classes: + - name: Plugin + methods: + - name: run + arguments: + - name: self +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.12 + interpreter: CPython +``` + +--- + +## 📄 filename + +```yaml +filename: extension.py +``` + +- Optional. +- Declares the filename of the module being validated. +- Used for reference and filtering in multi-module declarations. + +--- + +## 🔣 variables + +```yaml +variables: + - name: engine + value: docker +``` + +- Declares top-level variables that must be present in the importing module. +- Supports optional `annotation` (type hint). + +```yaml + - name: debug + annotation: bool + value: true +``` + +--- + +## 🧠 functions + +```yaml +functions: + - name: run + arguments: + - name: self + - name: config + annotation: dict + return_annotation: bool +``` + +- Declares standalone functions expected in the importing module. +- Use `arguments` and `return_annotation` for stricter typing. + +--- + +## 🧱 classes + +```yaml +classes: + - name: Plugin + attributes: + - type: class + name: plugin_name + value: my_plugin + methods: + - name: run + arguments: + - name: self + superclasses: + - name: BasePlugin +``` + +Each class can declare: + +- `attributes`: divided by `type` (`class` or `instance`) +- `methods`: each with `arguments` and optional `return_annotation` +- `superclasses`: flat list of required superclass names + +--- + +## 🧭 deployments + +This section defines where the module is allowed to run. + +```yaml +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.12.9 + interpreter: CPython + modules: + - filename: extension.py + version: 1.0.0 + variables: + - name: author + value: Luca Atella +``` + +### ✳️ Fields + +| Field | Type | Description | +|--------------|----------|--------------------------------------------| +| `arch` | Enum | e.g. `x86_64`, `arm64` | +| `os` | Enum | `linux`, `windows`, `darwin` | +| `version` | str | Python version string (`3.12.4`) | +| `interpreter`| Enum | `CPython`, `PyPy`, `IronPython`, etc. | +| `modules` | list | Repeats the structure declaration per module | + +This structure allows fine-grained targeting of supported environments. + +--- + +## 🌱 environment + +Environment variables and secrets expected on the system. + +```yaml +environment: + variables: + - name: LOG_LEVEL + value: INFO + - name: DEBUG + annotation: bool + value: true + secrets: + - MY_SECRET_KEY + - DATABASE_PASSWORD +``` + +- `variables`: can define name, value, and annotation +- `secrets`: only their presence is verified — values are never exposed + +--- + +## Notes + +- All fields are optional — contracts can be partial +- Field order does not matter +- Unknown fields are ignored with a warning (not an error) + +--- + +## Learn more + +- [Embedded Mode](../modes/embedded.md) +- [CLI Mode](../modes/cli.md) +- [Contract violations](../errors/index.md) diff --git a/docs/modes/embedded.md b/docs/modes/embedded.md new file mode 100644 index 0000000..83091a6 --- /dev/null +++ b/docs/modes/embedded.md @@ -0,0 +1,75 @@ +# Embedded Mode + +In Embedded Mode, ImportSpy is embedded directly into the module you want to protect. +When that module is imported, it inspects the runtime environment and the importing module. +If the context doesn't match the declared contract, the import fails with a structured error. + +--- + +## How it works + +By using `Spy().importspy(...)`, a protected module can validate: + +- The **runtime** (OS, Python version, architecture…) +- The **caller module’s structure** (classes, methods, variables, annotations…) + +If validation passes, the module returns a reference to the caller. +If not, the import is blocked and an exception is raised (e.g. `ValueError` or custom error class). + +--- + +## Real-world example: plugin-based architecture + +Let’s walk through a complete example. +This simulates a plugin framework that wants to validate the structure of external plugins at import time. + +### Project structure + +``` +external_module_compliance/ +├── extensions.py # The plugin (caller) +├── package.py # The protected framework +├── plugin_interface.py # Base interface for plugins +└── spymodel.yml # The import contract +``` + +--- + +### 🧩 Source files + +=== "package.py" + +```python +--8<-- "examples/plugin_based_architecture/external_module_compliance/package.py" +``` + +=== "extensions.py" + +```python +--8<-- "examples/plugin_based_architecture/external_module_compliance/extensions.py" +``` + +=== "spymodel.yml" + +```yaml +--8<-- "examples/plugin_based_architecture/external_module_compliance/spymodel.yml" +``` + +--- + +## When to use Embedded Mode + +Use this mode when: + +- You want to **protect a module** from being imported incorrectly +- You’re building a **plugin system** and expect structural consistency from plugins +- You want to **fail fast** in invalid environments +- You need to enforce custom logic during `import` without modifying the caller + +--- + +## Learn more + +- [→ CLI Mode](cli.md) +- [→ Contract syntax](../contracts/syntax.md) +- [→ Error types](../errors/index.md) diff --git a/docs/use_cases/index.md b/docs/use_cases/index.md new file mode 100644 index 0000000..5367f24 --- /dev/null +++ b/docs/use_cases/index.md @@ -0,0 +1,45 @@ +# Use Cases + +ImportSpy brings contract-based validation to dynamic Python environments, enabling control, predictability, and safety. + +Below are common scenarios where ImportSpy proves useful. + +--- + +## Embedded Mode in Plugin Architectures + +In plugin-based systems, a core module often exposes an interface or expected structure that plugins must follow. + +With ImportSpy embedded in the core, plugins are validated at import time to ensure they define the required classes, methods, variables, and environment conditions. + +This prevents silent failures or misconfigurations by enforcing structural and runtime constraints early. + +--- + +## CLI Validation in CI/CD Pipelines + +In DevOps pipelines or pre-release validation, ImportSpy can be run via CLI to ensure a Python module conforms to its declared import contract. + +This use case is especially valuable in: + +- Automated deployments +- Open-source projects with modular extensions +- Static validation of third-party contributions + +--- + +## Restricting Import Access by Runtime Context + +A module can use ImportSpy to refuse being imported unless specific runtime conditions are met — such as architecture, operating system, interpreter, or Python version. + +This enforces strict execution environments and prevents unintended usage in unsupported contexts. + +It's particularly helpful in: + +- Security-sensitive modules +- Packages intended for specific embedded systems +- Conditional feature sets based on runtime capabilities + +--- + +These use cases show how ImportSpy bridges runtime context and modular design through declarative contracts. diff --git a/mkdocs.yml b/mkdocs.yml index c97182f..427af83 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1 +1,53 @@ -site_name: My Docs +site_name: ImportSpy +#site_url: +repo_url: https://github.com/importspy/importspy +repo_name: importspy/importspy + +theme: + name: material + logo: assets/logo.svg + favicon: assets/favicon.ico + palette: + - scheme: default + primary: blue + accent: light blue + features: + - navigation.tabs + - navigation.instant + +nav: + - Home: index.md + - Introduction: + - What is ImportSpy: intro/overview.md + - Installation: intro/install.md + - Quickstart: intro/quickstart.md + - Operating Modes: + - Embedded Mode: modes/embedded.md + - CLI Mode: modes/cli.md + - YAML Contracts: + - Syntax: contracts/syntax.md + - Examples: contracts/examples.md + - Advanced Usage: + - CI/CD Integration: advanced/cicd.md + - Plugin Systems: advanced/plugins.md + - SpyModel Architecture: advanced/spymodel.md + - Violation System: advanced/violations.md + - Validation & Errors: validation-errors.md + - API Reference: api-reference.md + - Contributing: contribute.md + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + show_source: false + +markdown_extensions: + - pymdownx.snippets + - admonition + - pymdownx.tabbed: + alternate_style: true + + diff --git a/poetry.lock b/poetry.lock index b6a158f..4cd2ed6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -497,6 +497,25 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pymdown-extensions" +version = "10.16.1" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d"}, + {file = "pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91"}, +] + +[package.dependencies] +markdown = ">=3.6" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.19.1)"] + [[package]] name = "pytest" version = "8.4.1" @@ -869,4 +888,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "550dcabca1cba56ff54d8fc937ed16b4b328554be192a8641a4ac544985713bb" +content-hash = "32f9d75cf07ddd3bdd46f748776a754bf4e718de7e1331aeb9c93002bd2ea451" diff --git a/pyproject.toml b/pyproject.toml index 5b4618d..fcd1e92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ pydantic = "^2.9.2" ruamel-yaml = "^0.18.10" typer = "^0.15.2" mkdocs = "^1.6.1" +pymdown-extensions = "^10.16.1" [tool.poetry.group.dev.dependencies] From 7e2e7d7160a7b10e05c07597f768f3495a3a3916 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Sat, 2 Aug 2025 19:15:00 +0200 Subject: [PATCH 30/40] docs: add homepage, reference structure and violation system docs --- docs/advanced/spymodel.md | 85 ++++++++++++ docs/advanced/violations.md | 165 ++++++++++++++++++++++++ docs/api-reference.md | 119 +++++++++++++++++ docs/index.md | 153 ++++++++++++++++++++-- docs/use_cases/index.md | 101 +++++++++++++-- mkdocs.yml | 7 +- src/importspy/cli.py | 141 ++++++++++---------- src/importspy/config.py | 73 +++-------- src/importspy/constants.py | 119 ++++++----------- src/importspy/log_manager.py | 84 ++++-------- src/importspy/models.py | 199 ++++++++++++++--------------- src/importspy/persistences.py | 84 ++++++------ src/importspy/s.py | 128 ++++++++++--------- src/importspy/validators.py | 30 +---- src/importspy/violation_systems.py | 164 +++++++++++++++++------- 15 files changed, 1075 insertions(+), 577 deletions(-) create mode 100644 docs/advanced/spymodel.md create mode 100644 docs/advanced/violations.md diff --git a/docs/advanced/spymodel.md b/docs/advanced/spymodel.md new file mode 100644 index 0000000..b6ecd13 --- /dev/null +++ b/docs/advanced/spymodel.md @@ -0,0 +1,85 @@ +# SpyModel Architecture + +The **SpyModel** is the central object in ImportSpy's validation model. +It represents both the structural definition of a Python module and the contextual constraints under which the module can be imported and executed. + +This hybrid role makes it a bridge between the static world of code structure and the dynamic world of runtime validation. + +--- + +## Overview + +ImportSpy introduces the concept of **import contracts**: structured `.yml` files that describe how a Python module is expected to behave, what it provides, and where it is valid. + +The SpyModel is the object-oriented representation of these contracts. It is composed of two main aspects: + +- **Structural metadata**: variables, functions, classes, attributes, and type annotations +- **Deployment constraints**: supported architectures, operating systems, interpreters, Python versions, environment variables, and secrets + +This combination allows precise control over when and where a module can be imported. + +--- + +## Structural Layer + +The structural part of a SpyModel describes the internal shape of the Python module: + +- **filename**: the name of the `.py` file +- **variables**: global variables defined at module level, including optional type and value +- **functions**: standalone functions, with argument names, types, and return annotations +- **classes**: including attributes (class-level and instance-level), methods, and inheritance + +Each of these elements is validated against what the contract expects. +This ensures that a module importing another can rely on a well-defined structure. + +--- + +## Deployment Layer + +The second part of the model defines where and under which conditions the module is considered valid. + +This is handled by the `deployments` field, a list of accepted runtime configurations. +Each deployment includes: + +- **arch**: CPU architecture (e.g., `x86_64`, `ARM`) +- **systems**: operating systems supported (`linux`, `windows`, `macos`) + - Each system includes: + - **environment**: + - **variables**: required environment variables and their expected values + - **secrets**: variable names that must exist, without checking their value + - **pythons**: accepted Python interpreters and versions, each with: + - a list of expected **modules**, including their own structure + +This allows defining highly specific constraints such as: + +- “This plugin can only be used on Linux x86_64 with Python 3.12.9” +- “This module requires `MY_SECRET_KEY` to be present in the environment” + +--- + +## Schema Overview + +The following UML diagram summarizes the structure of the SpyModel and its relationships: + +![SpyModel UML](../assets/importspy-spy-model-architecture.png) + +Each node represents a data structure used during validation. +The model is hierarchical: from deployments down to classes and attributes, every element is traceable and verifiable. + +--- + +## Design Rationale + +The SpyModel is designed to be: + +- **Declarative**: the contract is expressed in data, not code logic +- **Versionable**: stored as YAML, the contract can be committed to Git +- **Composable**: it supports multiple deployment targets and alternative environments +- **Predictable**: ensures that structural mismatches are detected early + +By separating structure from logic, ImportSpy enables a contract-driven development workflow. +This is particularly useful in plugin frameworks, controlled environments, or distributed systems where consistency across modules and contexts is critical. + +--- + +ImportSpy treats contracts as **first-class citizens**. The SpyModel is the embodiment of this philosophy: a transparent, enforceable, and structured declaration of what a module requires and provides. diff --git a/docs/advanced/violations.md b/docs/advanced/violations.md new file mode 100644 index 0000000..badc68a --- /dev/null +++ b/docs/advanced/violations.md @@ -0,0 +1,165 @@ +# Violation System + +The Violation System in **ImportSpy** is responsible for surfacing clear, contextual, and actionable errors when a Python module fails to comply with its declared **import contract**. + +Rather than raising generic Python exceptions, this subsystem transforms validation failures into precise, domain-specific diagnostics that are structured, explainable, and safe to expose in both development and production contexts. + +--- + +## Purpose + +In modular and plugin-based systems, structural mismatches and runtime incompatibilities can lead to subtle bugs, hard crashes, or silent failures. + +The Violation System serves as a **contract enforcement layer** that: + +- Captures detailed context about the failure (module, scope, variable, system) +- Distinguishes between **missing**, **mismatched**, and **invalid** values +- Produces **human-readable** error messages tailored to developers and CI pipelines +- Structures error reporting consistently across validation layers (module, runtime, environment, etc.) + +--- + +## Core Concepts + +### 1. `ContractViolation` Interface + +An abstract base that defines the required interface for all contract violations. It ensures consistency across the different scopes (variables, functions, systems, etc.). + +```python +class ContractViolation(ABC): + @property + @abstractmethod + def context(self) -> str + + @abstractmethod + def label(self, spec: str) -> str + + @abstractmethod + def missing_error_handler(self, spec: str) -> str + + @abstractmethod + def mismatch_error_handler(self, spec: str) -> str + + @abstractmethod + def invalid_error_handler(self, spec: str) -> str +``` + +### 2. `BaseContractViolation` + +A reusable abstract class that implements common logic for generating violation messages (e.g. missing values, mismatches, invalid values) across all scopes. +It uses templates from `Errors` to construct consistent, high-fidelity diagnostics. + +```text +Example output: +[Module Validation Error]: Variable 'API_KEY' is missing. - Declare it in the expected module. +``` + +--- + +## Specific Violation Classes + +Each domain (Variable, Function, Runtime, etc.) has its own implementation: + +| Class | Purpose | +|--------------------------|----------------------------------------------------------| +| `VariableContractViolation` | Handles variable mismatches or missing values | +| `FunctionContractViolation` | Detects function mismatches and signature issues | +| `ModuleContractViolation` | Validates filename and version consistency | +| `RuntimeContractViolation` | Validates system architectures (`x86_64`, `arm64`, etc.)| +| `SystemContractViolation` | Checks operating system and environment variable match | +| `PythonContractViolation` | Validates interpreter and Python version compatibility | + +Each one specializes the `.label()` method to return error-specific context (e.g., "Environment variable `MY_SECRET` is missing in production runtime"). + +--- + +## Dynamic Payload Injection via `Bundle` + +Each violation operates with a shared mutable dictionary-like object called a **`Bundle`**: + +```python +@dataclass +class Bundle(MutableMapping): + state: Optional[dict[str, Any]] = field(default_factory=dict) +``` + +It serves two purposes: + +- Collect **contextual information** at validation time (e.g., variable name, expected type) +- Dynamically populate **templated error messages** using the `Errors` constant map + +This design allows error messages to adapt to the specific failure, with no hardcoding. + +--- + +## Error Categorization + +ImportSpy distinguishes between three primary error types: + +| Category | Description | +|--------------|--------------------------------------------------------------------------| +| `MISSING` | A required entity (class, method, variable) was not found | +| `MISMATCH` | An entity exists but its value, annotation, or structure differs | +| `INVALID` | A value exists but does not belong to the allowed set (e.g., OS types) | + +The `Errors` constant defines templates for all categories, per context: + +```yaml +"MISSING": + "module": + "template": "Expected module '{label}' is missing" + "solution": "Add the module to your import path" +``` + +--- + +## Engineering Highlights + +- **Encapsulation**: All formatting, message construction, and error typing is abstracted out of validators. +- **Separation of Concerns**: Validators focus only on logic, while violations handle messaging. +- **Templated Errors**: All violations draw from `Errors`, ensuring uniformity and easier localization or branding. +- **Composable Context**: The `Bundle` allows rich diagnostics without tight coupling between layers. + +--- + +## Example Usage + +```python +from importspy.violation_systems import VariableContractViolation + +bundle = Bundle() +bundle["expected_variable"] = "API_KEY" + +raise ValueError( + VariableContractViolation( + scope="module", + context="MODULE_CONTEXT", + bundle=bundle + ).missing_error_handler("entity") +) +``` + +Produces: + +```text +[Module Validation]: Variable 'API_KEY' is missing - Declare it in the target module. +``` + +--- + +## Future Extensions + +The Violation System is designed to be: + +- Extensible with new scopes (`Decorators`, `ReturnTypes`, `Dependencies`) +- Pluggable with i18n/l10n systems +- Renderable in **structured JSON** for machine processing in DevOps pipelines + +--- + +## Summary + +The Violation System is not just error handling — it is ImportSpy’s **engine of clarity**. +It converts abstract contract mismatches into structured, interpretable diagnostics that empower developers to catch errors before runtime. + +By modeling validation feedback as first-class entities, ImportSpy enables precise governance across modular codebases. diff --git a/docs/api-reference.md b/docs/api-reference.md index e69de29..c0ccbf1 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -0,0 +1,119 @@ +# API Reference + +This section documents the public Python API exposed by ImportSpy, organized by module. +All components listed here are available when you install the package. + +## `importspy.s` + +::: importspy.s + handler: python + options: + show_source: false + +--- + +## `importspy.models` + +::: importspy.models + handler: python + options: + show_source: false + +--- + +## `importspy.validators` + +::: importspy.validators + handler: python + options: + show_source: false + +--- + +## `importspy.violation_systems` + +::: importspy.violation_systems + handler: python + options: + show_source: false + +--- + +## `importspy.persistences` + +::: importspy.persistences + handler: python + options: + show_source: false + +--- + +## `importspy.cli` + +::: importspy.cli + handler: python + options: + show_source: false + +--- + +## `importspy.constants` + +::: importspy.constants + handler: python + options: + show_source: false + +--- + +## `importspy.config` + +::: importspy.config + handler: python + options: + show_source: false + +--- + +## `importspy.log_manager` + +::: importspy.log_manager + handler: python + options: + show_source: false + +--- + +## `importspy.utilities.module_util` + +::: importspy.utilities.module_util + handler: python + options: + show_source: false + +--- + +## `importspy.utilities.runtime_util` + +::: importspy.utilities.runtime_util + handler: python + options: + show_source: false + +--- + +## `importspy.utilities.system_util` + +::: importspy.utilities.system_util + handler: python + options: + show_source: false + +--- + +## `importspy.utilities.python_util` + +::: importspy.utilities.python_util + handler: python + options: + show_source: false diff --git a/docs/index.md b/docs/index.md index 000ea34..3904009 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,146 @@ -# Welcome to MkDocs +# ImportSpy -For full documentation visit [mkdocs.org](https://www.mkdocs.org). +**Context-aware import validation for Python modules** -## Commands +ImportSpy is an open-source Python library that introduces a robust mechanism to control and validate how and when modules are imported. At its core, it relies on versioned, declarative **import contracts** — written in YAML — which describe what a module expects from its execution context and its importer. -* `mkdocs new [dir-name]` - Create a new project. -* `mkdocs serve` - Start the live-reloading docs server. -* `mkdocs build` - Build the documentation site. -* `mkdocs -h` - Print help message and exit. +It brings **modularity**, **predictability**, and **security** to Python ecosystems. -## Project layout +--- - mkdocs.yml # The configuration file. - docs/ - index.md # The documentation homepage. - ... # Other markdown pages, images and other files. +## What is an Import Contract? + +An import contract is a `.yml` file that defines: + +- The expected **structure** of a module: functions, classes, arguments, annotations, variables +- The allowed **environments**: OS, architecture, Python version, interpreter +- Optional conditions on runtime **environment variables** or **superclasses** + +If these conditions are not met, ImportSpy can stop the import and raise a detailed, structured error — before any runtime failure can occur. + +--- + +## Key Features + +- YAML-based **import contracts** +- Embedded and CLI-based **validation** +- Structural validation of **variables**, **functions**, **classes** +- Runtime checks for **OS**, **architecture**, **Python version**, **interpreter** +- Contract-driven plugin validation for **secure extensibility** +- Clear, explainable **error reporting** on mismatch, missing, or invalid usage +- Fully integrable in **CI/CD pipelines** + +--- + +## Use Cases + +ImportSpy is built for teams that need: + +- **Plugin-based architectures** with strict interface enforcement +- **Runtime protection** against incompatible environments +- Early validation in **DevSecOps** or **regulatory** pipelines +- Defensive boundaries between **internal components** +- **Automated structure verification** during deployment + +--- + +## Example: Embedded Mode + +```python +from importspy import Spy + +caller = Spy().importspy(filepath="contracts/spymodel.yml") +caller.MyPlugin().run() +``` + +--- + +## Example: CLI Mode + +```bash +importspy src/mymodule.py -s contracts/spymodel.yml --log-level DEBUG +``` + +--- + +## Project Structure + +ImportSpy is built around 3 key components: + +- `SpyModel`: represents the structural and runtime definition of a module +- `Spy`: the validation engine that compares real vs expected modules +- `Violation System`: formal system for raising errors with human-readable messages + +--- + +## Documentation Overview + +### 👣 Get Started + +- [Quickstart](intro/quickstart.md) +- [Install](intro/install.md) +- [Overview](intro/overview.md) + +### ⚙️ Modes of Operation + +- [Embedded Mode](modes/embedded.md) +- [CLI Mode](modes/cli.md) + +### 📄 Import Contracts + +- [Contract Syntax](contracts/syntax.md) +- [SpyModel Specification](advanced/spymodel.md) + +### 🧠 Validation Engine + +- [Violation System](advanced/violations.md) +- [Contract Violations](errors/contract_violations.md) +- [Error Table](errors/error_table.md) + +### 📦 Use Cases + +- [Plugin-based Architectures](use_cases/index.md) + +### 📘 API Reference + +- [API Docs](api-reference.md) + +### 🤝 Contributing + +- [Contributing Guidelines](../CONTRIBUTING.md) +- [Security Policy](../SECURITY.md) +- [License (MIT)](../LICENSE) + +--- + +## Architecture Diagram + +![SpyModel UML](../assets/importspy-spy-model-architecture.png) + +--- + +## Why ImportSpy? + +Python’s import system is powerful, but not context-aware. ImportSpy solves this by adding a **layer of structural governance** and **runtime filtering**. + +This makes it ideal for: + +- Plugin systems +- Isolated runtimes +- Package compliance +- Security-aware applications +- CI enforcement of expected module interfaces + +--- + +## Sponsorship & Community + +If ImportSpy is useful in your infrastructure, help us grow by: + +- [Starring the project on GitHub](https://github.com/your-org/importspy) +- [Becoming a GitHub Sponsor](https://github.com/sponsors/your-org) +- [Contributing modules, tests, or docs](../CONTRIBUTING.md) + +--- + +> ImportSpy is more than a validator — it's a contract of trust between Python modules. diff --git a/docs/use_cases/index.md b/docs/use_cases/index.md index 5367f24..0f18c3d 100644 --- a/docs/use_cases/index.md +++ b/docs/use_cases/index.md @@ -10,36 +10,111 @@ Below are common scenarios where ImportSpy proves useful. In plugin-based systems, a core module often exposes an interface or expected structure that plugins must follow. -With ImportSpy embedded in the core, plugins are validated at import time to ensure they define the required classes, methods, variables, and environment conditions. +With ImportSpy embedded in the core, plugins are validated **at import time** to ensure they define the required classes, methods, variables, and environment conditions. This prevents silent failures or misconfigurations by enforcing structural and runtime constraints early. +!!! example "Example: Plugin Enforced at Import" + ```python + --8<-- "examples/plugin_based_architecture/package.py" + ``` + +See also [Embedded Mode](../../modes/embedded.md) and [Contract Syntax](../../contracts/syntax.md) for YAML details. + --- ## CLI Validation in CI/CD Pipelines -In DevOps pipelines or pre-release validation, ImportSpy can be run via CLI to ensure a Python module conforms to its declared import contract. +In DevOps workflows or during pre-release validation, ImportSpy can be executed from the command line to ensure a Python module conforms to its declared import contract. + +Typical use cases: +- Automated deployment verification +- Open-source plugin contributions +- Validating extension points in modular codebases -This use case is especially valuable in: +!!! example "Example: Validate via CLI" + ```bash + importspy extensions.py -s spymodel.yml -l INFO + ``` -- Automated deployments -- Open-source projects with modular extensions -- Static validation of third-party contributions +See [CLI Mode](../../modes/cli.md) for full usage. --- ## Restricting Import Access by Runtime Context -A module can use ImportSpy to refuse being imported unless specific runtime conditions are met — such as architecture, operating system, interpreter, or Python version. +A module can refuse to be imported unless specific runtime conditions are met — such as CPU architecture, OS, Python version, or interpreter. + +This enables: +- Targeted deployments (e.g., Linux-only, CPython-only) +- Restriction to known-safe execution environments +- Fail-fast behavior in unsupported contexts + +Contracts can define system constraints like: + +```yaml +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.12 + interpreter: CPython +``` + +If the runtime doesn't match, import fails with a clear error message. + +--- + +## Contract-as-Code: Executable Documentation + +ImportSpy contracts double as living specifications for module structure and environment assumptions. + +Rather than maintaining separate interface docs, a `.yml` contract acts as: + +- ✅ Interface specification +- ✅ Compatibility schema +- ✅ Runtime validator + +This approach improves communication in: +- Plugin-based systems +- Collaborative teams sharing Python APIs +- Educational or onboarding contexts + +!!! example "Example Contract Snippet" + ```yaml + classes: + - name: Plugin + methods: + - name: run + arguments: + - name: self + ``` + +Any contributor can run `importspy` or trigger the embedded validation to ensure conformance. + +--- + +## Supporting Multiple Deployment Targets + +Modules may need to support more than one runtime environment (e.g., Linux and Windows, or Python 3.10+). -This enforces strict execution environments and prevents unintended usage in unsupported contexts. +ImportSpy contracts support listing **multiple valid deployments**, each with its own OS, interpreter, and version constraints. -It's particularly helpful in: +```yaml +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.10 + - os: windows + pythons: + - version: 3.11 +``` -- Security-sensitive modules -- Packages intended for specific embedded systems -- Conditional feature sets based on runtime capabilities +This enables the same module to be validated across CI matrices or downstream consumers with differing setups. --- -These use cases show how ImportSpy bridges runtime context and modular design through declarative contracts. +Together, these use cases show how ImportSpy bridges **runtime context and modular structure** through declarative contracts — empowering safer, more predictable Python architectures. diff --git a/mkdocs.yml b/mkdocs.yml index 427af83..6722008 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,15 +1,14 @@ site_name: ImportSpy -#site_url: +site_url: https://importspy.github.io/importspy/ repo_url: https://github.com/importspy/importspy repo_name: importspy/importspy theme: name: material - logo: assets/logo.svg - favicon: assets/favicon.ico + logo: assets/importspy-logo.png palette: - scheme: default - primary: blue + primary: purple accent: light blue features: - navigation.tabs diff --git a/src/importspy/cli.py b/src/importspy/cli.py index 837fb5a..db25877 100644 --- a/src/importspy/cli.py +++ b/src/importspy/cli.py @@ -1,57 +1,27 @@ """ -Command-line interface for ImportSpy import contract validation. +Command-line interface (CLI) for validating Python modules against ImportSpy contracts. -This module defines the CLI entry point `importspy`, which enables developers and CI/CD pipelines -to validate Python modules against a declared import contract written in YAML format. +This module defines the `importspy` CLI command, enabling local and automated validation +of a Python file against a YAML-based SpyModel contract. It is designed for use in +CI/CD pipelines, plugin systems, or developer workflows. -Overview: ---------- -The CLI allows structural and runtime compliance checks through: -- Dynamic import of a Python module from file. -- Parsing of the import contract from a `.yml` file. -- Validation of the module’s structure and metadata. -- Clear CLI feedback with styled messages. -- Optional log verbosity control for debugging purposes. +Features: +- Loads and executes the specified Python module. +- Parses the YAML contract file describing expected structure and runtime conditions. +- Validates that the module complies with the declared interface and environment. +- Provides user-friendly CLI feedback, including optional logging. -Main Command: -------------- -- `importspy`: Validates a Python module against an import contract definition. +Use cases: +- Enforcing structure of external plugins before loading. +- Automating validation in GitHub Actions or other CI tools. +- Assuring consistency in modular libraries or educational tools. -Decorators: ------------ -- `handle_validation_error`: Intercepts and formats validation errors - to improve user experience from the terminal. +Example: + importspy ./examples/my_plugin.py -s ./contracts/expected.yml --log-level DEBUG -Usage Examples: ---------------- -Basic validation: - -.. code-block:: bash - - importspy ./examples/my_module.py - -With contract and log level: - -.. code-block:: bash - - importspy ./my_module.py --spymodel contracts/example.yml --log-level DEBUG - -Options: --------- ---spymodel / -s : str - Path to the YAML file containing the import contract. Default: `spymodel.yml`. - ---log-level / -l : str - Log verbosity. Accepts: DEBUG, INFO, WARNING, ERROR. - ---version / -v - Show ImportSpy’s current version. - -Notes: ------- -- Validation is handled by the `Spy` core class. -- This command is ideal for local development, CI enforcement, or release pipelines. -- Validation issues are surfaced through color-coded output, not raw exceptions. +Note: + Validation is powered by the core `Spy` class. + Validation errors are caught and displayed with enhanced CLI formatting. """ import typer @@ -70,17 +40,20 @@ def handle_validation_error(func): """ - Intercepts validation errors and formats them for CLI output. + Decorator that formats validation errors for CLI output. - Provides color-coded feedback based on validation result. + Intercepts `ValueError` raised by the `Spy.importspy()` call and presents + the error reason in a readable, styled terminal message. + + Used to wrap the main `importspy()` CLI command. """ @functools.wraps(func) def wrapper(*args, **kwargs): try: func(*args, **kwargs) - typer.echo(typer.style("✅ Module is compliant with the import contract!", fg=typer.colors.GREEN, bold=True)) + typer.echo(typer.style("Module is compliant with the import contract.", fg=typer.colors.GREEN, bold=True)) except ValueError as ve: - typer.echo(typer.style("❌ Module is NOT compliant with the import contract.", fg=typer.colors.RED, bold=True)) + typer.echo(typer.style("Module is NOT compliant with the import contract.", fg=typer.colors.RED, bold=True)) typer.echo() typer.secho("Reason:", fg="magenta", bold=True) typer.echo(f" {typer.style(str(ve), fg='yellow')}") @@ -97,24 +70,45 @@ class LogLevel(str, Enum): @app.command() @handle_validation_error def importspy( - version: Optional[bool] = typer.Option( - None, - "--version", - "-v", - callback=lambda value: show_version(value), - is_eager=True, - help="Show the version and exit." - ), - modulepath: Optional[str] = typer.Argument(str, help="Path to the Python module to load and validate."), - spymodel_path: Optional[str] = typer.Option( - "spymodel.yml", "--spymodel", "-s", help="Path to the import contract file (.yml)." - ), - log_level: Optional[LogLevel] = typer.Option( - None, "--log-level", "-l", help="Log level for output verbosity." - ) + version: Optional[bool] = typer.Option( + None, + "--version", + "-v", + callback=lambda value: show_version(value), + is_eager=True, + help="Show the version and exit." + ), + modulepath: Optional[str] = typer.Argument( + str, + help="Path to the Python module to load and validate." + ), + spymodel_path: Optional[str] = typer.Option( + "spymodel.yml", + "--spymodel", + "-s", + help="Path to the import contract file (.yml)." + ), + log_level: Optional[LogLevel] = typer.Option( + None, + "--log-level", + "-l", + help="Log level for output verbosity." + ) ) -> ModuleType: """ - CLI command to validate a Python module against a YAML-defined import contract. + Validates a Python module against a YAML-defined SpyModel contract. + + Args: + version (bool, optional): Show ImportSpy version and exit. + modulepath (str): Path to the Python module to validate. + spymodel_path (str, optional): Path to the YAML contract file. Defaults to `spymodel.yml`. + log_level (LogLevel, optional): Set logging verbosity (DEBUG, INFO, WARNING, ERROR). + + Returns: + ModuleType: The validated Python module (if compliant). + + Raises: + ValueError: If the module does not conform to the contract. """ module_path = Path(modulepath).resolve() module_name = module_path.stem @@ -131,7 +125,10 @@ def importspy( def show_version(value: bool): """ - Displays the current ImportSpy version and exits. + Displays the current version of ImportSpy and exits the process. + + Args: + value (bool): If True, prints the version and exits immediately. """ if value: typer.secho(f"ImportSpy v{__version__}", fg="cyan", bold=True) @@ -139,6 +136,10 @@ def show_version(value: bool): def main(): """ - Entry point for CLI execution. + CLI entry point. + + Executes the `importspy` Typer app, allowing CLI usage like: + + $ importspy my_module.py -s my_contract.yml """ app() diff --git a/src/importspy/config.py b/src/importspy/config.py index d1da905..1f875b3 100644 --- a/src/importspy/config.py +++ b/src/importspy/config.py @@ -1,63 +1,32 @@ class Config: + """ - Static configuration container for ImportSpy. + Static configuration for ImportSpy. - This class centralizes all constant values used for runtime validation, - defining supported architectures, operating systems, Python versions, - interpreter implementations, attribute classifications, and annotation types. - These constants ensure consistency and safety during contract enforcement - across diverse execution environments. + This class defines the baseline constants used during runtime and structural + validation of Python modules. Values declared here represent the supported + options for platforms, interpreters, Python versions, class attribute types, + and type annotations within a SpyModel contract. - Attributes: - ARCH_x86_64 (str): Identifier for the x86_64 CPU architecture. - ARCH_AARCH64 (str): Identifier for the AArch64 architecture. - ARCH_ARM (str): Identifier for the ARM architecture. - ARCH_ARM64 (str): Identifier for the ARM64 architecture. - ARCH_I386 (str): Identifier for the i386 (32-bit Intel) architecture. - ARCH_PPC64 (str): Identifier for the PowerPC 64-bit architecture. - ARCH_PPC64LE (str): Identifier for PowerPC 64-bit Little Endian architecture. - ARCH_S390X (str): Identifier for IBM s390x architecture. + These constants are used internally to validate compatibility and enforce + declared constraints across diverse environments. - OS_WINDOWS (str): Identifier for Windows operating systems. - OS_LINUX (str): Identifier for Linux operating systems. - OS_MACOS (str): Identifier for macOS (Darwin-based) operating systems. + Categories: + ---------- + • Architectures: CPU instruction sets (e.g. x86_64, arm64). + • Operating Systems: Target OS identifiers (e.g. linux, windows). + • Python Versions: Compatible interpreter versions. + • Interpreters: Supported Python implementations. + • Attribute Types: Class vs. instance variables. + • Type Annotations: Accepted runtime-compatible types. - PYTHON_VERSION_3_13 (str): Supported Python version 3.13. - PYTHON_VERSION_3_12 (str): Supported Python version 3.12. - PYTHON_VERSION_3_11 (str): Supported Python version 3.11. - PYTHON_VERSION_3_10 (str): Supported Python version 3.10. - PYTHON_VERSION_3_9 (str): Supported Python version 3.9. - - INTERPRETER_CPYTHON (str): Identifier for CPython interpreter. - INTERPRETER_PYPY (str): Identifier for PyPy interpreter. - INTERPRETER_JYTHON (str): Identifier for Jython interpreter. - INTERPRETER_IRON_PYTHON (str): Identifier for IronPython interpreter. - INTERPRETER_STACKLESS (str): Identifier for Stackless Python. - INTERPRETER_MICROPYTHON (str): Identifier for MicroPython interpreter. - INTERPRETER_BRYTHON (str): Identifier for Brython interpreter. - INTERPRETER_PYSTON (str): Identifier for Pyston interpreter. - INTERPRETER_GRAALPYTHON (str): Identifier for GraalPython interpreter. - INTERPRETER_RUSTPYTHON (str): Identifier for RustPython interpreter. - INTERPRETER_NUITKA (str): Identifier for Nuitka interpreter. - INTERPRETER_TRANSCRYPT (str): Identifier for Transcrypt interpreter. - - CLASS_TYPE (str): Label for class-level attributes in contract definitions. - INSTANCE_TYPE (str): Label for instance-level attributes in contract definitions. - - ANNOTATION_INT (str): Annotation identifier for integers. - ANNOTATION_FLOAT (str): Annotation identifier for floats. - ANNOTATION_STR (str): Annotation identifier for strings. - ANNOTATION_BOOL (str): Annotation identifier for booleans. - ANNOTATION_LIST (str): Annotation identifier for generic lists. - ANNOTATION_DICT (str): Annotation identifier for generic dictionaries. - ANNOTATION_TUPLE (str): Annotation identifier for generic tuples. - ANNOTATION_SET (str): Annotation identifier for sets. - ANNOTATION_OPTIONAL (str): Annotation identifier for optional values. - ANNOTATION_UNION (str): Annotation identifier for union types. - ANNOTATION_ANY (str): Annotation identifier for untyped (any) values. - ANNOTATION_CALLABLE (str): Annotation identifier for callable objects. + Examples: + --------- + - A contract may require `arch: x86_64` and `interpreter: CPython`. + - A method argument may be annotated with `Optional[str]`. """ + # Supported Architectures ARCH_x86_64 = "x86_64" ARCH_AARCH64 = "aarch64" diff --git a/src/importspy/constants.py b/src/importspy/constants.py index 650708b..28442e8 100644 --- a/src/importspy/constants.py +++ b/src/importspy/constants.py @@ -4,36 +4,36 @@ class Constants: """ - Constants used internally by ImportSpy's runtime validation engine. + Canonical constants used by ImportSpy's runtime validation engine. - This class defines the canonical reference values used during import contract - validation, including supported architectures, operating systems, Python - interpreters, annotation types, and structural metadata keys. + This class acts as a reference for valid architectures, operating systems, + Python interpreters, structural types, and annotation labels. All values + defined here represent the allowed forms of metadata used to verify + import contracts. - Unlike `Config`, which defines values dynamically from the runtime or user - environment, `Constants` serves as the fixed baseline for what ImportSpy - considers valid and contract-compliant. + Unlike `Config`, which reflects the current runtime, `Constants` provides + the fixed set of values used for validation logic. """ class SupportedArchitectures(str, Enum): - - ARCH_x86_64 = Config.ARCH_x86_64 - ARCH_AARCH64 = Config.ARCH_AARCH64 + """Valid CPU architectures accepted in contracts.""" + ARCH_x86_64 = Config.ARCH_x86_64 + ARCH_AARCH64 = Config.ARCH_AARCH64 ARCH_ARM = Config.ARCH_ARM - ARCH_ARM64 = Config.ARCH_ARM64 - ARCH_I386 = Config.ARCH_I386 - ARCH_PPC64 = Config.ARCH_PPC64 - ARCH_PPC64LE = Config.ARCH_PPC64LE + ARCH_ARM64 = Config.ARCH_ARM64 + ARCH_I386 = Config.ARCH_I386 + ARCH_PPC64 = Config.ARCH_PPC64 + ARCH_PPC64LE = Config.ARCH_PPC64LE ARCH_S390X = Config.ARCH_S390X class SupportedOS(str, Enum): - + """Valid operating systems accepted in contracts.""" OS_WINDOWS = Config.OS_WINDOWS - OS_LINUX = Config.OS_LINUX + OS_LINUX = Config.OS_LINUX OS_MACOS = Config.OS_MACOS class SupportedPythonImplementations(str, Enum): - + """Valid Python interpreter implementations.""" INTERPRETER_CPYTHON = Config.INTERPRETER_CPYTHON INTERPRETER_PYPY = Config.INTERPRETER_PYPY INTERPRETER_JYTHON = Config.INTERPRETER_JYTHON @@ -45,9 +45,9 @@ class SupportedPythonImplementations(str, Enum): INTERPRETER_RUSTPYTHON = Config.INTERPRETER_RUSTPYTHON INTERPRETER_NUITKA = Config.INTERPRETER_NUITKA INTERPRETER_TRANSCRYPT = Config.INTERPRETER_TRANSCRYPT - - class SupportedClassAttributeTypes(str, Enum): + class SupportedClassAttributeTypes(str, Enum): + """Type of attribute in a class-level contract.""" CLASS = Config.CLASS_TYPE INSTANCE = Config.INSTANCE_TYPE @@ -59,6 +59,7 @@ class SupportedClassAttributeTypes(str, Enum): INSTANCE_TYPE = Config.INSTANCE_TYPE class SupportedAnnotations(str, Enum): + """Supported type annotations for validation purposes.""" INT = Config.ANNOTATION_INT FLOAT = Config.ANNOTATION_FLOAT STR = Config.ANNOTATION_STR @@ -76,20 +77,29 @@ class SupportedAnnotations(str, Enum): TUPLE_TYPING = Config.ANNOTATION_TUPLE_TYPING LOG_MESSAGE_TEMPLATE = ( - "[Operation: {operation}] [Status: {status}] " - "[Details: {details}]" + "[Operation: {operation}] [Status: {status}] [Details: {details}]" ) + class Contexts(str, Enum): + """ + Context types used for contract validation. + These labels identify which layer of the system the error or constraint applies to. + """ RUNTIME_CONTEXT = "runtime" ENVIRONMENT_CONTEXT = "environment" MODULE_CONTEXT = "module" CLASS_CONTEXT = "class" + class Errors: """ - Defines reusable templates for error generation. + Reusable error string templates and labels. + + This utility provides consistent formatting for all error messages + generated by ImportSpy. It supports singular and plural forms, as well as + different validation categories: missing, mismatch, and invalid. """ TEMPLATE_KEY = "template" @@ -108,109 +118,75 @@ class Errors: } class Category(str, Enum): - + """Validation error types.""" MISSING = "missing" MISMATCH = "mismatch" INVALID = "invalid" + # Label formatting templates for each contract context RUNTIME_LABEL_TEMPLATE = { - ENTITY_MESSAGES: 'The runtime "{runtime_1}"', COLLECTIONS_MESSAGES: 'The runtimes "{runtimes_1}"' - } SYSTEM_LABEL_TEMPLATE = { - ENTITY_MESSAGES: 'The system "{system_1}"', COLLECTIONS_MESSAGES: 'systems "{systems_1}"' - } PYTHON_LABEL_TEMPLATE = { - ENTITY_MESSAGES: 'The python "{python_1}"', COLLECTIONS_MESSAGES: 'The pythons "{pythons_1}"' - } VARIABLES_LABEL_TEMPLATE = { - SCOPE_VARIABLE: { - ENTITY_MESSAGES: { - Contexts.ENVIRONMENT_CONTEXT: 'The environment variable "{environment_variable_name}"', Contexts.MODULE_CONTEXT: 'The variable "{variable_name}" in module "{module_name}"', Contexts.CLASS_CONTEXT: 'The {attribute_type} attribute "{attribute_name}" in class "{class_name}"' - }, - COLLECTIONS_MESSAGES: { - Contexts.ENVIRONMENT_CONTEXT: 'The environment "{environment_1}"', Contexts.MODULE_CONTEXT: 'The variables "{variables_1}"', Contexts.CLASS_CONTEXT: 'The attributes "{attributes_1}"' - } }, - SCOPE_ARGUMENT: { - ENTITY_MESSAGES: { - Contexts.MODULE_CONTEXT: 'The argument "{argument_name}" of function "{function_name}"', - Contexts.CLASS_CONTEXT: 'The argument "{argument_name}" of method "{method_name}" in class "{class_name}"', - + Contexts.CLASS_CONTEXT: 'The argument "{argument_name}" of method "{method_name}" in class "{class_name}"' }, - COLLECTIONS_MESSAGES: { - Contexts.MODULE_CONTEXT: 'The arguments "{arguments_1}" of function "{function_name}"', Contexts.CLASS_CONTEXT: 'The arguments "{arguments_1}" of method "{method_name}", in class "{class_name}"' - } - } - } FUNCTIONS_LABEL_TEMPLATE = { - ENTITY_MESSAGES: { - Contexts.MODULE_CONTEXT: 'The function "{function_name}" in module "{filename}"', Contexts.CLASS_CONTEXT: 'The method "{method_name}" in class "{class_name}"' - }, - COLLECTIONS_MESSAGES: { - Contexts.MODULE_CONTEXT: 'The functions "{functions_1}" in module "{filename}"', Contexts.CLASS_CONTEXT: 'The methods "{methods_1}" in class "{class_name}"' - } - } MODULE_LABEL_TEMPLATE = { - ENTITY_MESSAGES: { - Contexts.CLASS_CONTEXT: 'The class "{class_name}"', Contexts.RUNTIME_CONTEXT: 'The module "{filename}"', Contexts.ENVIRONMENT_CONTEXT: 'The version "{version}" of module "{filename}"' - }, - COLLECTIONS_MESSAGES: { - Contexts.CLASS_CONTEXT: 'The classes "{classes_1}" in module "{filename}"' - } - } + # Dynamic variable keys used in label formatting KEY_RUNTIMES_1 = "runtimes_1" KEY_SYSTEMS_1 = "systems_1" KEY_PYTHONS_1 = "pythons_1" @@ -236,60 +212,40 @@ class Category(str, Enum): KEY_MODULE_VERSION = "version" KEY_FILE_NAME = "filename" + # Dynamic template values used to build error labels VARIABLES_DINAMIC_PAYLOAD = { - SCOPE_VARIABLE: { - ENTITY_MESSAGES: { - Contexts.ENVIRONMENT_CONTEXT: KEY_ENVIRONMENT_VARIABLE_NAME, Contexts.MODULE_CONTEXT: KEY_VARIABLE_NAME, Contexts.CLASS_CONTEXT: KEY_ATTRIBUTE_NAME - }, - COLLECTIONS_MESSAGES: { - Contexts.ENVIRONMENT_CONTEXT: KEY_ENVIRONMENT_1, Contexts.MODULE_CONTEXT: KEY_VARIABLES_1, Contexts.CLASS_CONTEXT: KEY_ATTRIBUTES_1 - } }, SCOPE_ARGUMENT: { - ENTITY_MESSAGES: { - Contexts.MODULE_CONTEXT: KEY_ARGUMENT_NAME, Contexts.CLASS_CONTEXT: KEY_ARGUMENT_NAME - }, - COLLECTIONS_MESSAGES: { - Contexts.MODULE_CONTEXT: KEY_ARGUMENTS_1, Contexts.CLASS_CONTEXT: KEY_ARGUMENTS_1 - } - } - } FUNCTIONS_DINAMIC_PAYLOAD = { - ENTITY_MESSAGES: { - Contexts.MODULE_CONTEXT: KEY_FUNCTION_NAME, Contexts.CLASS_CONTEXT: KEY_METHOD_NAME - }, - COLLECTIONS_MESSAGES: { - Contexts.MODULE_CONTEXT: KEY_FUNCTIONS_1, Contexts.CLASS_CONTEXT: KEY_METHODS_1 - } } @@ -325,4 +281,3 @@ class Category(str, Enum): } } } - diff --git a/src/importspy/log_manager.py b/src/importspy/log_manager.py index c0ca8fb..754592a 100644 --- a/src/importspy/log_manager.py +++ b/src/importspy/log_manager.py @@ -22,19 +22,15 @@ class CustomFormatter(logging.Formatter): This formatter extends the default logging format by appending the exact filename, line number, and function name where each log was triggered. - This is especially useful in distributed architectures, plugin-based systems, - or debugging deeply nested calls during module inspection. - Format: ------- - ``[timestamp] [LEVEL] [logger name] [caller: file, line, function] message`` + [timestamp] [LEVEL] [logger name] + [caller: file, line, function] message Example: -------- - .. code-block:: text - - 2024-02-24 14:30:12 [INFO] [my_logger] - [caller: example.py, line: 42, function: my_function] This is a log message. + 2024-02-24 14:30:12 [INFO] [my_logger] + [caller: example.py, line: 42, function: my_function] This is a log message. """ LOG_FORMAT = ( @@ -51,22 +47,17 @@ def __init__(self): def format(self, record): """ - Adds caller details to the log record. - - Enhances logs with: - - Filename where the log was triggered - - Line number - - Function name + Enriches the log record with caller information. Parameters: ----------- record : logging.LogRecord - The original log event. + The original log record to be formatted. Returns: -------- str - The enriched, formatted log message. + A fully formatted log message including file, line, and function context. """ record.caller_file = record.pathname.split("/")[-1] record.caller_line = record.lineno @@ -78,37 +69,28 @@ class LogManager: """ Centralized manager for all logging within ImportSpy. - This class ensures that: - - All loggers use the same format (`CustomFormatter`) - - Logging is only configured once to avoid duplication - - Each component of the framework can retrieve its own scoped logger - - Whether ImportSpy runs embedded inside another module or as a CLI tool, - the `LogManager` ensures that log output is clean, traceable, and standardized. + This class ensures: + - Uniform formatting across all loggers + - Avoidance of duplicate configuration + - Consistent output in both CLI and embedded contexts Attributes: ----------- default_level : int - The system's current log level at the time of instantiation. + The current log level derived from the root logger. default_handler : logging.StreamHandler - Default output handler using `CustomFormatter`. + Default handler for logging output, using the `CustomFormatter`. configured : bool - Indicates whether global logging has already been configured. - - Methods: - -------- - - `configure(level, handlers)`: Applies global settings to the root logger. - - `get_logger(name)`: Retrieves a logger with consistent formatting and context. + Whether the logging system has already been configured. """ def __init__(self): """ - Sets up default logging options. + Sets up the default logging handler and format. - The default handler uses ImportSpy’s `CustomFormatter` and logs to `stdout`. - Logging is deferred until explicitly configured. + Uses `CustomFormatter` and logs to standard output by default. """ self.default_level = logging.getLogger().getEffectiveLevel() self.default_handler = logging.StreamHandler() @@ -117,30 +99,22 @@ def __init__(self): def configure(self, level: int = None, handlers: list = None): """ - Configures the global logging system. + Applies logging configuration globally. - This method attaches handlers to the root logger and sets the global level. - It must be called only once to avoid duplicate logs or handler conflicts. + Prevents duplicate setup. This method should be called once per application. Parameters: ----------- level : int, optional - Desired log level (e.g., `logging.DEBUG` or `logging.INFO`). - Defaults to the system’s current level. + Log level (e.g., logging.DEBUG). Defaults to current system level. handlers : list of logging.Handler, optional - List of custom handlers to attach. If omitted, uses the default stream handler. + Custom handlers to use. Falls back to `default_handler` if none provided. Raises: ------- RuntimeError - If logging has already been configured elsewhere in the application. - - Example: - -------- - .. code-block:: python - - LogManager().configure(level=logging.DEBUG) + If logging is already configured. """ if self.configured: raise RuntimeError("LogManager has already been configured.") @@ -158,27 +132,19 @@ def configure(self, level: int = None, handlers: list = None): def get_logger(self, name: str) -> logging.Logger: """ - Retrieves a scoped logger configured with ImportSpy’s formatting. + Returns a named logger with ImportSpy's formatting applied. - This logger is safe to use across modules and plugins. - It ensures no duplicate handlers and maintains the current log level. + Ensures the logger is properly configured and ready for use. Parameters: ----------- name : str - Name of the logger (typically `__name__` or class name). + The name of the logger (e.g., a module name). Returns: -------- logging.Logger - A configured logger ready for use. - - Example: - -------- - .. code-block:: python - - logger = LogManager().get_logger("my_module") - logger.info("Validation complete.") + The initialized logger instance. """ logger = logging.getLogger(name) if not logger.handlers: diff --git a/src/importspy/models.py b/src/importspy/models.py index c4f99e3..15b0b73 100644 --- a/src/importspy/models.py +++ b/src/importspy/models.py @@ -1,21 +1,18 @@ """ -importspy.models ----------------- +models.py +========== -This module defines the core data models used by ImportSpy for contract-based -runtime validation of Python modules. It includes structural representations -of variables, functions, classes, and full modules, as well as runtime and -system-level metadata required to enforce import contracts across execution contexts. +Defines the structural and contextual data models used across ImportSpy. +These models represent modules, variables, functions, classes, runtimes, +systems, and environments involved in contract-based validation. + +This module powers both embedded validation and CLI checks, enabling ImportSpy +to introspect, serialize, and enforce compatibility rules at multiple levels: +from source code structure to runtime platform details. """ from pydantic import BaseModel - -from typing import ( - Optional, - Union, - List -) - +from typing import Optional, Union, List from types import ModuleType from .utilities.module_util import ( @@ -25,14 +22,8 @@ from .utilities.runtime_util import RuntimeUtil from .utilities.system_util import SystemUtil from .utilities.python_util import PythonUtil -from .constants import ( - Constants, - Contexts, - Errors -) - +from .constants import Constants, Contexts, Errors from .config import Config - import logging logger = logging.getLogger("/".join(__file__.split('/')[-2:])) @@ -41,10 +32,13 @@ class Python(BaseModel): """ - Represents a specific Python runtime configuration. + Represents a Python runtime environment. - Includes the Python version, interpreter type, and the list of loaded modules. - Used to validate compatibility between caller and callee environments. + Includes: + - Python version + - Interpreter type (e.g., CPython, PyPy) + - List of loaded modules + Used in validating runtime compatibility. """ version: Optional[str] = None interpreter: Optional[Constants.SupportedPythonImplementations] = None @@ -52,28 +46,35 @@ class Python(BaseModel): def __str__(self): return f"{self.interpreter} v{self.version}" - + def __repr__(self): return str(self) + class Environment(BaseModel): """ - Represents a set of environment variables and secret keys - defined for the system or application runtime. + Represents runtime environment variables and secrets. + Used for validating runtime configuration. """ variables: Optional[list['Variable']] = None secrets: Optional[list[str]] = None def __str__(self): return f"variables: {self.variables} | secrets: {self.secrets}" - + def __repr__(self): return str(self) + class System(BaseModel): """ - Represents the system environment, including OS, environment variables, - and Python runtimes configured within the system. + Represents a full OS environment within a deployment system. + + Includes: + - OS type + - Environment variables + - Python runtimes + Used to validate cross-platform compatibility. """ os: Constants.SupportedOS environment: Optional[Environment] = None @@ -81,30 +82,36 @@ class System(BaseModel): def __str__(self): return f"{self.os.value}" - + def __repr__(self): return str(self) class Runtime(BaseModel): """ - Represents the deployment runtime, identified by CPU architecture and - the list of supported systems associated with that architecture. + Represents a runtime deployment context. + + Defined by CPU architecture and associated systems. """ arch: Constants.SupportedArchitectures systems: list[System] def __str__(self): return f"{self.arch}" - + def __repr__(self): return str(self) class Variable(BaseModel): """ - Represents a declared variable within a Python module, including optional type - annotation and value. Used for structural validation of the importing module. + Represents a top-level variable in a Python module. + + Includes: + - Name + - Optional annotation + - Optional static value + Used to enforce structural consistency. """ name: str annotation: Optional[Constants.SupportedAnnotations] = None @@ -112,36 +119,31 @@ class Variable(BaseModel): @classmethod def from_variable_info(cls, variables_info: list[VariableInfo]): - """ - Convert a list of extracted VariableInfo into Variable instances. - """ - return [Variable( + return [cls( name=var_info.name, value=var_info.value, annotation=var_info.annotation ) for var_info in variables_info] - + def __str__(self): type_part = f": {self.annotation}" if self.annotation else "" return f"{self.name}{type_part} = {self.value}" - + def __repr__(self): return str(self) class Attribute(Variable): """ - Represents a class attribute, extending Variable with a 'type' indicator - (e.g., 'class', 'instance'). Used in class-level contract validation. + Represents a class-level attribute. + + Extends Variable with attribute type (e.g., class or instance). """ type: Constants.SupportedClassAttributeTypes @classmethod def from_attributes_info(cls, attributes_info: list[AttributeInfo]): - """ - Convert a list of AttributeInfo objects into Attribute instances. - """ - return [Attribute( + return [cls( type=attr_info.type, name=attr_info.name, value=attr_info.value, @@ -151,15 +153,17 @@ def from_attributes_info(cls, attributes_info: list[AttributeInfo]): class Argument(Variable, BaseModel): """ - Represents a function argument, including its name, type annotation, and default value. - Used to validate callable structures and type consistency. + Represents a function/method argument. + + Includes: + - Name + - Optional type annotation + - Optional default value + Used to check call signatures. """ @classmethod def from_arguments_info(cls, arguments_info: list[ArgumentInfo]): - """ - Convert a list of ArgumentInfo into Argument instances. - """ - return [Argument( + return [cls( name=arg_info.name, annotation=arg_info.annotation, value=arg_info.value @@ -168,8 +172,12 @@ def from_arguments_info(cls, arguments_info: list[ArgumentInfo]): class Function(BaseModel): """ - Represents a callable function, including its name, argument signature, - and return type annotation. + Represents a callable entity. + + Includes: + - Name + - List of arguments + - Optional return annotation """ name: str arguments: Optional[list[Argument]] = None @@ -177,27 +185,29 @@ class Function(BaseModel): @classmethod def from_functions_info(cls, functions_info: list[FunctionInfo]): - """ - Convert a list of FunctionInfo into Function instances. - """ - return [Function( + return [cls( name=func_info.name, arguments=Argument.from_arguments_info(func_info.arguments), return_annotation=func_info.return_annotation ) for func_info in functions_info] - + def __str__(self): - formatted_arguments = f"{', '.join(str(arg) for arg in self.arguments)}" if self.arguments else "" - return f"{self.name}({formatted_arguments}) -> {self.return_annotation}" - + args = ", ".join(str(arg) for arg in self.arguments) if self.arguments else "" + return f"{self.name}({args}) -> {self.return_annotation}" + def __repr__(self): return str(self) class Class(BaseModel): """ - Represents a Python class, including its attributes, methods, and declared superclasses. - Used to enforce object-level validation rules in contracts. + Represents a Python class declaration. + + Includes: + - Name + - Attributes (class/instance) + - Methods + - Superclasses (recursive) """ name: str attributes: Optional[list[Attribute]] = None @@ -206,20 +216,17 @@ class Class(BaseModel): @classmethod def from_class_info(cls, extracted_classes: list[ClassInfo]): - """ - Convert a list of extracted class definitions into Class instances. - """ - return [Class( + return [cls( name=name, attributes=Attribute.from_attributes_info(attributes), methods=Function.from_functions_info(methods), - superclasses= cls.from_class_info(superclasses) + superclasses=cls.from_class_info(superclasses) ) for name, attributes, methods, superclasses in extracted_classes] - + def get_class_attributes(self) -> List[Attribute]: if self.attributes: return [attr for attr in self.attributes if attr.type == Config.CLASS_TYPE] - + def get_instance_attributes(self) -> List[Attribute]: if self.attributes: return [attr for attr in self.attributes if attr.type == Config.INSTANCE_TYPE] @@ -227,8 +234,12 @@ def get_instance_attributes(self) -> List[Attribute]: class Module(BaseModel): """ - Represents a full Python module, including its filename, version, - and all its internal components (variables, functions, classes). + Represents a Python module. + + Includes: + - Filename + - Version (if extractable) + - Top-level variables, functions, and classes """ filename: Optional[str] = None version: Optional[str] = None @@ -238,25 +249,25 @@ class Module(BaseModel): def __str__(self): return f"Module: {self.filename or 'unknown'} (v{self.version or '-'})" - + def __repr__(self): return str(self) class SpyModel(Module): """ - Extends the base Module structure with additional deployment metadata. + High-level model used by ImportSpy for validation. - SpyModel is the top-level object representing a module's structure and its - runtime/environment constraints. This is the core of ImportSpy's contract model. + Extends the module representation with runtime metadata and + platform-specific deployment constraints (architecture, OS, interpreter, etc). """ deployments: Optional[list[Runtime]] = None @classmethod def from_module(cls, info_module: ModuleType): """ - Create a SpyModel from a loaded Python module, extracting its metadata - and attaching runtime/system context. + Build a SpyModel instance by extracting structure and metadata + from an actual Python module object. """ module_utils = ModuleUtil() runtime_utils = RuntimeUtil() @@ -313,32 +324,16 @@ def from_module(cls, info_module: ModuleType): ] ) + class Error(BaseModel): + """ + Describes a structured validation error. + Includes the context, error type, message, and resolution steps. + Used to serialize feedback during contract enforcement. + """ context: Contexts title: str category: Errors.Category description: str solution: str - -""" - @classmethod - def from_contract_violation(cls, contract_violation: BaseContractViolation): - tpl = Errors.ERROR_MESSAGE_TEMPLATES.get(contract_violation.category) - title = Errors.CONTEXT_INTRO.get(contract_violation.context) - description = tpl[Errors.TEMPLATE_KEY].format(label=contract_violation.label) - solution = tpl[Errors.SOLUTION_KEY] - return cls( - context=contract_violation.context, - title=title, - category=contract_violation.category, - description=description, - solution=solution - ) - - def render_message(self) -> str: - return f"[{self.title}] {self.description} {self.solution}" -""" - - - diff --git a/src/importspy/persistences.py b/src/importspy/persistences.py index ebd9a43..099f85d 100644 --- a/src/importspy/persistences.py +++ b/src/importspy/persistences.py @@ -1,16 +1,12 @@ """ -importspy.persistences -======================= - -This module defines the interfaces and implementations for handling **import contracts** — +Defines interfaces and implementations for handling **import contracts** — external YAML files used by ImportSpy to validate the structure and runtime expectations of dynamically loaded Python modules. -Currently, YAML is the only supported contract format, but the architecture is fully -extensible via the `Parser` interface. +Currently, only YAML is supported, but the architecture is extensible via the `Parser` interface. -All file access operations are wrapped in safe error handling using `handle_persistence_error`, -which raises human-readable exceptions when contract files are missing, corrupted, or unreadable. +All file I/O operations are wrapped in `handle_persistence_error`, ensuring clear error +messages in case of missing, malformed, or inaccessible contract files. """ from abc import ABC, abstractmethod @@ -20,86 +16,84 @@ class Parser(ABC): """ - Abstract interface for import contract parsers. + Abstract base class for import contract parsers. - A contract parser is responsible for loading and saving `.yml` files - that describe the expected structure of a Python module. This abstraction - allows ImportSpy to support multiple formats (e.g., YAML, JSON, TOML) in the future. + Parsers are responsible for loading and saving `.yml` contract files that define + a module’s structural and runtime expectations. This abstraction enables future + support for additional formats (e.g., JSON, TOML). - Subclasses must implement both `save()` and `load()` methods. + Subclasses must implement `save()` and `load()`. """ @abstractmethod def save(self, data: dict, filepath: str): """ - Serializes the given import contract (as a Python dictionary) and writes it to a file. + Serializes the contract (as a dictionary) and writes it to disk. Parameters: ----------- data : dict - A dictionary representation of the import contract. + Dictionary containing the contract structure. filepath : str - The path where the contract should be saved (usually with `.yml` extension). + Target path for saving the contract (typically `.yml`). """ pass @abstractmethod def load(self, filepath: str) -> dict: """ - Loads and parses an import contract from a file into a Python dictionary. + Parses a contract file and returns it as a dictionary. Parameters: ----------- filepath : str - Path to the `.yml` contract file. + Path to the contract file on disk. Returns: -------- dict - The parsed contract as a dictionary. + Parsed contract data. """ pass class PersistenceError(Exception): """ - Custom exception raised when there is a problem reading or writing import contracts. + Raised when contract loading or saving fails due to I/O or syntax issues. - This error wraps low-level I/O or parsing issues and presents them in a way - that is meaningful to end users. + This exception wraps low-level errors and provides human-readable feedback. """ def __init__(self, msg: str): """ - Initializes the `PersistenceError`. + Initialize the error with a descriptive message. Parameters: ----------- msg : str - A human-readable error message describing the failure. + Explanation of the failure. """ super().__init__(msg) def handle_persistence_error(func): """ - Decorator that wraps file I/O operations in safe error handling. - - If the decorated function raises any exception (e.g., file not found, malformed YAML), - a `PersistenceError` is raised with a descriptive message instead. + Decorator for wrapping parser I/O methods with user-friendly error handling. - This helps ensure that ImportSpy fails gracefully during contract handling. + Catches all exceptions and raises a `PersistenceError` with a generic message. + This ensures ImportSpy fails gracefully if a contract file is missing, + malformed, or inaccessible. Parameters: ----------- func : Callable - The function to decorate. + The I/O method to wrap. Returns: -------- Callable - The wrapped function. + A wrapped version that raises `PersistenceError` on failure. """ @functools.wraps(func) def wrapper(*args, **kwargs): @@ -115,26 +109,26 @@ def wrapper(*args, **kwargs): class YamlParser(Parser): """ - YAML-based implementation of the `Parser` interface. + YAML-based contract parser implementation. - This parser reads and writes import contracts from `.yml` files using the `ruamel.yaml` library. - It preserves indentation, flow style, and quotes to ensure consistent structure across validations. + Uses `ruamel.yaml` to read and write `.yml` files that define import contracts. + Preserves formatting, indentation, and quotes for consistent serialization. """ def __init__(self): """ - Initializes the YAML parser and applies default formatting rules for readability. + Initializes the YAML parser and configures output formatting. """ self.yaml = YAML() self._yml_configuration() def _yml_configuration(self): """ - Applies consistent formatting to YAML output: + Applies formatting rules to YAML output: - - Disables flow style for better readability - - Sets indentation rules for mappings and sequences - - Preserves quotes for exact string representation + - Disables flow style + - Sets consistent indentation + - Preserves quotes in strings """ self.yaml.default_flow_style = False self.yaml.indent(mapping=2, sequence=4, offset=2) @@ -143,15 +137,15 @@ def _yml_configuration(self): @handle_persistence_error def save(self, data: dict, filepath: str): """ - Saves an import contract to a `.yml` file. + Saves a contract dictionary to a `.yml` file. Parameters: ----------- data : dict - The contract content as a dictionary. + Contract structure. filepath : str - The output path where the YAML file will be saved. + Destination file path. """ with open(filepath, "w") as file: self.yaml.dump(data, file) @@ -159,17 +153,17 @@ def save(self, data: dict, filepath: str): @handle_persistence_error def load(self, filepath: str) -> dict: """ - Loads an import contract from a `.yml` file and parses it into a dictionary. + Loads and parses a `.yml` contract into a Python dictionary. Parameters: ----------- filepath : str - Path to the YAML file. + Path to the contract file. Returns: -------- dict - A Python dictionary containing the contract structure. + Parsed contract structure. """ with open(filepath) as file: data = self.yaml.load(file) diff --git a/src/importspy/s.py b/src/importspy/s.py index aea802e..8d38433 100644 --- a/src/importspy/s.py +++ b/src/importspy/s.py @@ -1,7 +1,4 @@ """ -importspy.s -=========== - Core validation logic for ImportSpy. This module defines the `Spy` class, the central component responsible for dynamically @@ -32,7 +29,6 @@ SystemValidator, PythonValidator, ModuleValidator - ) from .log_manager import LogManager from .persistences import Parser, YamlParser @@ -41,7 +37,6 @@ List ) import logging - from .violation_systems import ( Bundle, ModuleContractViolation, @@ -49,51 +44,37 @@ SystemContractViolation, PythonContractViolation ) - from .constants import Contexts + class Spy: """ - The `Spy` class is the core engine of ImportSpy — it handles validation, introspection, - and enforcement of structural contracts for Python modules. - - This class is designed to support both: + Core validation engine for ImportSpy. - - **Embedded validation**, where it is imported and executed inside the module under control. - - **CLI-based or pipeline validation**, where an external tool invokes Spy programmatically. + The `Spy` class is responsible for loading a target module, extracting its structure, + and validating it against a YAML-based import contract. This ensures that the importing + module satisfies all declared structural and runtime constraints. - ImportSpy uses declarative **import contracts**, written as human-readable YAML files, - to describe what a valid module should contain. These contracts define expected classes, - attributes, methods, and even environmental constraints (like Python version or OS). - - The `Spy` class dynamically loads the target module, extracts its metadata, and checks - for compliance against the contract. If validation fails, descriptive errors are raised - before the module can be used improperly. + It supports two modes: + - **Embedded mode**: validates the caller of the current module + - **External/CLI mode**: validates an explicitly provided module Attributes: ----------- logger : logging.Logger - Logger instance used to track validation steps and internal processing. + Structured logger for validation diagnostics. parser : Parser - Parser responsible for loading the import contract from disk (currently supports YAML). - - Methods: - -------- - - `__init__()` → Initializes logger and default parser. - - `importspy(filepath, log_level, info_module)` → Validates a specified or inferred module. - - `_configure_logging(log_level)` → Sets logging level based on user/system config. - - `_validate_module(contract, info_module)` → Compares a module to the contract definition. - - `_inspect_module()` → Introspects the call stack to locate the calling module. + Parser used to load import contracts (defaults to YAML). """ def __init__(self): """ - Initializes the `Spy` instance. + Initialize the Spy instance. - This method sets up: - - the logging system for capturing all validation and introspection steps - - the default parser (`YamlParser`) for loading `.yml` import contracts + Sets up: + - a dedicated logger + - the default YAML parser """ self.logger = LogManager().get_logger(self.__class__.__name__) self.parser: Parser = YamlParser() @@ -103,35 +84,34 @@ def importspy(self, log_level: Optional[int] = None, info_module: Optional[ModuleType] = None) -> ModuleType: """ - Loads and validates a Python module based on an import contract. + Main entry point for validation. - This is the primary method used to validate a module, whether in embedded mode - (by inspecting the importer), or in external mode (via CLI or script). + Loads and validates a Python module against the contract defined in the given YAML file. + If no module is explicitly provided, introspects the call stack to infer the caller. Parameters: ----------- filepath : Optional[str] - Path to the `.yml` contract file defining the expected structure. + Path to the `.yml` import contract. log_level : Optional[int] - Logging level for output verbosity. Uses system default if not provided. + Log verbosity level (e.g., `logging.DEBUG`). info_module : Optional[ModuleType] - Optional reference to the module to validate. If not provided, - the calling module is inferred via stack inspection. + The module to validate. If `None`, uses the importer via stack inspection. Returns: -------- ModuleType - The module that was validated. + The validated module. Raises: ------- RuntimeError - If logging is misconfigured or reconfigured unexpectedly. + If logging setup fails. ValueError - If a recursion pattern is detected (e.g., a module validating itself). + If recursion is detected (e.g., a module is validating itself). """ self._configure_logging(log_level) spymodel: SpyModel = SpyModel(**self.parser.load(filepath=filepath)) @@ -141,15 +121,15 @@ def importspy(self, def _configure_logging(self, log_level: Optional[int] = None): """ - Configures ImportSpy's logging system for runtime use. + Set up logging for validation. - If a log level is provided, it overrides the system's default. This method ensures - the logger is only configured once, preventing duplicate log handlers. + If not already configured, applies the provided or default log level + using ImportSpy’s centralized logging system. Parameters: ----------- log_level : Optional[int] - The desired log level (e.g., logging.INFO, logging.DEBUG). + Logging level to use (e.g., `logging.INFO`, `logging.DEBUG`). """ log_manager = LogManager() if not log_manager.configured: @@ -157,41 +137,65 @@ def _configure_logging(self, log_level: Optional[int] = None): log_manager.configure(level=log_level or system_log_level) def _validate_module(self, spymodel: SpyModel, info_module: ModuleType) -> ModuleType: + """ + Perform all validation steps against the loaded module. + + This includes contract-level, runtime, system, and Python environment checks. + All contract violations are collected in a `Bundle`. + + Parameters: + ----------- + spymodel : SpyModel + The expected contract loaded from file. + + info_module : ModuleType + The actual module to inspect and validate. + + Returns: + -------- + ModuleType + The validated module, reloaded after introspection. + """ self.logger.debug(f"info_module: {info_module}") if spymodel: bundle = Bundle() - module_validator:ModuleValidator = ModuleValidator() + module_validator = ModuleValidator() self.logger.debug(f"Import contract detected: {spymodel}") spy_module = SpyModel.from_module(info_module) self.logger.debug(f"Extracted module structure: {spy_module}") - module_contract: ModuleContractViolation = ModuleContractViolation(Contexts.MODULE_CONTEXT, bundle) - module_validator.validate([spymodel],spy_module.deployments[0].systems[0].pythons[0].modules[0], module_contract) - runtime_contract: RuntimeContractViolation = RuntimeContractViolation(Contexts.RUNTIME_CONTEXT, bundle) - runtime:Runtime = RuntimeValidator().validate(spymodel.deployments, spy_module.deployments, runtime_contract) - system_contract: SystemContractViolation = SystemContractViolation(Contexts.RUNTIME_CONTEXT, bundle) - pythons:List[Python] = SystemValidator().validate(runtime.systems, spy_module.deployments[0].systems, system_contract) - python_contract: PythonContractViolation = PythonContractViolation(Contexts.RUNTIME_CONTEXT, bundle) - modules: List[Module] = PythonValidator().validate(pythons, spy_module.deployments[0].systems[0].pythons, python_contract) + + module_contract = ModuleContractViolation(Contexts.MODULE_CONTEXT, bundle) + module_validator.validate([spymodel], spy_module.deployments[0].systems[0].pythons[0].modules[0], module_contract) + + runtime_contract = RuntimeContractViolation(Contexts.RUNTIME_CONTEXT, bundle) + runtime = RuntimeValidator().validate(spymodel.deployments, spy_module.deployments, runtime_contract) + + system_contract = SystemContractViolation(Contexts.RUNTIME_CONTEXT, bundle) + pythons = SystemValidator().validate(runtime.systems, spy_module.deployments[0].systems, system_contract) + + python_contract = PythonContractViolation(Contexts.RUNTIME_CONTEXT, bundle) + modules = PythonValidator().validate(pythons, spy_module.deployments[0].systems[0].pythons, python_contract) + module_validator.validate(modules, spy_module.deployments[0].systems[0].pythons[0].modules[0], module_contract) + return ModuleUtil().load_module(info_module) def _inspect_module(self) -> ModuleType: """ - Introspects the call stack to determine which module called `importspy()`. + Infer the module that invoked validation (embedded mode). - This is used primarily in embedded mode to locate the external plugin - or module that triggered the validation. It prevents the system from - analyzing itself (recursive inspection). + This prevents a module from validating itself and ensures that + ImportSpy targets the correct caller in the stack. Returns: -------- ModuleType - The module that imported or triggered validation. + The inferred external module. Raises: ------- ValueError - If recursion is detected (i.e., the same module is inspecting itself). + If a module attempts to validate itself. """ module_util = ModuleUtil() current_frame, caller_frame = module_util.inspect_module() diff --git a/src/importspy/validators.py b/src/importspy/validators.py index d93f000..1833b4c 100644 --- a/src/importspy/validators.py +++ b/src/importspy/validators.py @@ -62,7 +62,7 @@ def validate( Contexts.RUNTIME_CONTEXT, bundle ).missing_error_handler(Errors.COLLECTIONS_MESSAGES)) - + class SystemValidator: @@ -165,7 +165,7 @@ def validate( if self._is_python_match(python_1, python_2, contract_violation): return python_1.modules - + raise ValueError( PythonContractViolation( Contexts.RUNTIME_CONTEXT, @@ -178,29 +178,6 @@ def _is_python_match( python_2: Python, contract_violation: PythonContractViolation ) -> bool: - """ - Determine whether two Python configurations match. - - Parameters - ---------- - python_1 : Python - Expected configuration. - python_2 : Python - Actual system configuration. - - Returns - ------- - bool - `True` if the two configurations match according to the declared criteria, - otherwise `False`. - - Matching Criteria - ----------------- - - If both version and interpreter are defined: match both. - - If only version is defined: match version. - - If only interpreter is defined: match interpreter. - - If none are defined: match anything (default `True`). - """ bundle: Bundle = contract_violation.bundle bundle[Errors.KEY_PYTHON_1] = python_1 if python_1.version and python_1.interpreter: @@ -289,7 +266,6 @@ def validate( ) ) - class ClassValidator: def __init__(self): @@ -522,4 +498,4 @@ def validate( status="Completed", details="Validation successful." ) - ) \ No newline at end of file + ) diff --git a/src/importspy/violation_systems.py b/src/importspy/violation_systems.py index 8d923b4..460e010 100644 --- a/src/importspy/violation_systems.py +++ b/src/importspy/violation_systems.py @@ -1,50 +1,70 @@ -from abc import ( - ABC, - abstractmethod -) +""" +This module defines the hierarchy of contract violation classes used by ImportSpy. -from collections.abc import MutableMapping +Each violation type corresponds to a validation context (e.g., environment, runtime, module structure), +and provides structured, human-readable error messages when the importing module does not meet the +contract’s requirements. -from dataclasses import ( - dataclass, - field -) +The base interface `ContractViolation` defines the common error interface, while specialized classes +like `VariableContractViolation` or `RuntimeContractViolation` define formatting logic for each scope. -from typing import ( - Optional, - Any, - Iterator -) +Violations carry a dynamic `Bundle` object, which collects contextual metadata needed for +formatting error messages and debugging failed imports. +""" +from abc import ABC, abstractmethod +from collections.abc import MutableMapping +from dataclasses import dataclass, field +from typing import Optional, Any, Iterator from .constants import Errors + class ContractViolation(ABC): + """ + Abstract base interface for all contract violations. + + Defines the core methods for rendering structured error messages, + including context resolution and label generation. + + Properties: + ----------- + - `context`: Validation context (e.g., environment, class, runtime) + - `label(spec)`: Retrieves the field name or reference used in error text. + - `missing_error_handler(spec)`: Formats error when required entity is missing. + - `mismatch_error_handler(expected, actual, spec)`: Formats error when values differ. + - `invalid_error_handler(allowed, found, spec)`: Formats error when a value is invalid. + """ @property @abstractmethod def context(self) -> str: pass - + @abstractmethod - def label(self, spec:str) -> str: + def label(self, spec: str) -> str: pass - + @abstractmethod - def missing_error_handler(self, spec:str) -> str: + def missing_error_handler(self, spec: str) -> str: pass @abstractmethod - def mismatch_error_handler(self, spec:str) -> str: + def mismatch_error_handler(self, expected: Any, actual: Any, spec: str) -> str: pass @abstractmethod - def invalid_error_handler(self, spec:str) -> str: + def invalid_error_handler(self, allowed: Any, found: Any, spec: str) -> str: pass + class BaseContractViolation(ContractViolation): + """ + Base implementation of a contract violation. + + Includes default implementations of error formatting methods. + """ - def __init__(self, context, bundle:'Bundle'): - + def __init__(self, context: str, bundle: 'Bundle'): self._context = context self.bundle = bundle super().__init__() @@ -52,72 +72,118 @@ def __init__(self, context, bundle:'Bundle'): @property def context(self) -> str: return self._context - - def missing_error_handler(self, spec:str) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][spec][Errors.TEMPLATE_KEY].format(label=self.label(spec))} - {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][spec][Errors.SOLUTION_KEY].capitalize()}' - def mismatch_error_handler(self, expected:Any, actual:Any, spec:str) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][spec][Errors.TEMPLATE_KEY].format(label=self.label(spec), expected=expected, actual=actual)} - {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][spec][Errors.SOLUTION_KEY].capitalize()}' + def missing_error_handler(self, spec: str) -> str: + return ( + f"{Errors.CONTEXT_INTRO[self.context]}: " + f"{Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][spec][Errors.TEMPLATE_KEY].format(label=self.label(spec))} - " + f"{Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISSING][spec][Errors.SOLUTION_KEY].capitalize()}" + ) + + def mismatch_error_handler(self, expected: Any, actual: Any, spec: str) -> str: + return ( + f"{Errors.CONTEXT_INTRO[self.context]}: " + f"{Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][spec][Errors.TEMPLATE_KEY].format(label=self.label(spec), expected=expected, actual=actual)} - " + f"{Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.MISMATCH][spec][Errors.SOLUTION_KEY].capitalize()}" + ) + + def invalid_error_handler(self, allowed: Any, found: Any, spec: str) -> str: + return ( + f"{Errors.CONTEXT_INTRO[self.context]}: " + f"{Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][spec][Errors.TEMPLATE_KEY].format(label=self.label(spec), expected=allowed, actual=found)} - " + f"{Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][spec][Errors.SOLUTION_KEY].capitalize()}" + ) - def invalid_error_handler(self, allowed:Any, found:Any, spec:str) -> str: - return f'{Errors.CONTEXT_INTRO[self.context]}: {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][spec][Errors.TEMPLATE_KEY].format(label=self.label(spec), expected=allowed, actual=found)} - {Errors.ERROR_MESSAGE_TEMPLATES[Errors.Category.INVALID][spec][Errors.SOLUTION_KEY].capitalize()}' class VariableContractViolation(BaseContractViolation): + """ + Contract violation handler for variables (module, class, environment, etc.). - def __init__(self, scope:str, context:str, bundle:'Bundle'): + Includes scope information to distinguish between types of variables. + """ + + def __init__(self, scope: str, context: str, bundle: 'Bundle'): super().__init__(context, bundle) self.scope = scope - - def label(self, spec:str) -> str: + + def label(self, spec: str) -> str: return Errors.VARIABLES_LABEL_TEMPLATE[self.scope][spec][self.context].format(**self.bundle) + class FunctionContractViolation(BaseContractViolation): + """ + Contract violation handler for function signature mismatches. + """ - def __init__(self, context:str, bundle:'Bundle'): + def __init__(self, context: str, bundle: 'Bundle'): super().__init__(context, bundle) - - def label(self, spec:str) -> str: + + def label(self, spec: str) -> str: return Errors.FUNCTIONS_LABEL_TEMPLATE[spec][self.context].format(**self.bundle) + class RuntimeContractViolation(BaseContractViolation): + """ + Contract violation handler for runtime architecture mismatches. + """ - def __init__(self, context:str, bundle:'Bundle'): + def __init__(self, context: str, bundle: 'Bundle'): super().__init__(context, bundle) - - def label(self, spec:str) -> str: + + def label(self, spec: str) -> str: return Errors.RUNTIME_LABEL_TEMPLATE[spec].format(**self.bundle) + class SystemContractViolation(BaseContractViolation): + """ + Contract violation handler for system-level mismatches (OS, environment variables). + """ - def __init__(self, context:str, bundle:'Bundle'): + def __init__(self, context: str, bundle: 'Bundle'): super().__init__(context, bundle) - - def label(self, spec:str) -> str: + + def label(self, spec: str) -> str: return Errors.SYSTEM_LABEL_TEMPLATE[spec].format(**self.bundle) + class PythonContractViolation(BaseContractViolation): + """ + Contract violation handler for Python version and interpreter mismatches. + """ - def __init__(self, context:str, bundle:'Bundle'): + def __init__(self, context: str, bundle: 'Bundle'): super().__init__(context, bundle) - - def label(self, spec:str) -> str: + + def label(self, spec: str) -> str: return Errors.PYTHON_LABEL_TEMPLATE[spec].format(**self.bundle) + class ModuleContractViolation(BaseContractViolation): + """ + Contract violation handler for module-level mismatches (filename, version, structure). + """ - def __init__(self, context:str, bundle:'Bundle'): + def __init__(self, context: str, bundle: 'Bundle'): super().__init__(context, bundle) - - def label(self, spec:str) -> str: + + def label(self, spec: str) -> str: return Errors.MODULE_LABEL_TEMPLATE[spec][self.context].format(**self.bundle) + @dataclass class Bundle(MutableMapping): + """ + Shared mutable state passed to all violation handlers. + + The bundle is a dynamic container used to inject contextual values + (like module name, attribute name, or class name) into error templates. + """ + state: Optional[dict[str, Any]] = field(default_factory=dict) def __getitem__(self, key): return self.state[key] - + def __setitem__(self, key, value): self.state[key] = value @@ -131,4 +197,4 @@ def __len__(self) -> int: return len(self.state) def __repr__(self): - return repr(self.state) \ No newline at end of file + return repr(self.state) From 4a0212023ab34b3ccdb3ae1ba03530285df9b37d Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Sat, 2 Aug 2025 19:18:42 +0200 Subject: [PATCH 31/40] docs(readme): convert README from reStructuredText to Markdown Switched from .rst to .md format for better readability and compatibility with GitHub's native markdown renderer. Includes updated badges, architecture diagram, usage modes, and installation instructions. --- README.md | 178 +++++++++++++++++++++++++++++++++++++++++++++++++ README.rst | 190 ----------------------------------------------------- 2 files changed, 178 insertions(+), 190 deletions(-) create mode 100644 README.md delete mode 100644 README.rst diff --git a/README.md b/README.md new file mode 100644 index 0000000..e8ce70f --- /dev/null +++ b/README.md @@ -0,0 +1,178 @@ +# ImportSpy + +![License](https://img.shields.io/github/license/atellaluca/importspy) +[![PyPI Version](https://img.shields.io/pypi/v/importspy)](https://pypi.org/project/importspy/) +![Python Versions](https://img.shields.io/pypi/pyversions/importspy) +[![Build Status](https://img.shields.io/github/actions/workflow/status/atellaluca/ImportSpy/python-package.yml?branch=main)](https://github.com/atellaluca/ImportSpy/actions/workflows/python-package.yml) +[![Docs](https://img.shields.io/readthedocs/importspy)](https://importspy.readthedocs.io/) + +![ImportSpy banner](./assets/importspy-banner.png) + +**Runtime contract validation for Python imports.** +_Enforce structure. Block invalid usage. Stay safe at runtime._ + +--- + +## 🔍 What is ImportSpy? + +**ImportSpy** lets your Python modules declare structured **import contracts** (via `.yml` files) to define: + +- What environment they expect (OS, Python version, interpreter) +- What structure they must follow (classes, methods, variables) +- Who is allowed to import them + +If the contract is not met, **ImportSpy blocks the import** — ensuring safe and predictable runtime behavior. + +--- + +## ✨ Key Features + +- ✅ Validate imports dynamically at runtime or via CLI +- ✅ Block incompatible usage of internal or critical modules +- ✅ Enforce module structure, arguments, annotations +- ✅ Context-aware: Python version, OS, architecture, interpreter +- ✅ Human-readable YAML contracts +- ✅ Clear, CI-friendly violation messages + +--- + +## 📦 Installation + +```bash +pip install importspy +``` + +> Requires Python 3.10+ + +--- + +## 📐 Architecture + +![SpyModel UML](./assets/importspy-spy-model-architecture.png) + +ImportSpy is powered by a layered introspection model (`SpyModel`), which captures: + +- `Runtime`: CPU architecture +- `System`: OS and environment +- `Python`: interpreter and version +- `Module`: classes, functions, variables, annotations + +Each layer is validated against the corresponding section of your `.yml` contract. + +--- + +## 📜 Example Contract + +```yaml +filename: plugin.py +variables: + - name: mode + value: production + annotation: str +classes: + - name: Plugin + methods: + - name: run + arguments: + - name: self + - name: data + annotation: dict + return_annotation: None +``` + +--- + +## 🔧 Modes of Use + +### Embedded Mode – protect your own module + +```python +from importspy import Spy + +caller = Spy().importspy(filepath="spymodel.yml") +caller.Plugin().run() +``` + +![Embedded mode](./assets/importspy-embedded-mode.png) + +--- + +### CLI Mode – external validation in CI + +```bash +importspy -s spymodel.yml -l DEBUG path/to/module.py +``` + +![CLI mode](./assets/importspy-works.png) + +--- + +## 🧠 How It Works + +1. You define an import contract in `.yml` +2. At runtime or via CLI, ImportSpy inspects: + - Who is importing the module + - What the system/environment looks like + - What the module structure provides +3. If validation fails → the import is blocked +4. If valid → the module runs safely + +--- + +## ✅ Tech Stack + +- [Pydantic 2.x](https://docs.pydantic.dev) – schema validation +- [Typer](https://typer.tiangolo.com) – CLI +- [ruamel.yaml](https://yaml.readthedocs.io/) – YAML support +- `inspect` + `sys` – runtime introspection +- [Poetry](https://python-poetry.org) – dependency management +- [Sphinx](https://www.sphinx-doc.org) + ReadTheDocs – documentation + +--- + +## 📘 Documentation + +- **Full docs** → [importspy.readthedocs.io](https://importspy.readthedocs.io) +- [Quickstart](https://importspy.readthedocs.io/en/latest/intro/quickstart.html) +- [Contract syntax](https://importspy.readthedocs.io/en/latest/contracts/syntax.html) +- [Violation system](https://importspy.readthedocs.io/en/latest/advanced/violations.html) +- [API Reference](https://importspy.readthedocs.io/en/latest/api-reference.html) + +--- + +## 🚀 Ideal Use Cases + +- Plugin-based frameworks (e.g., CMS, CLI, IDE) +- CI/CD pipelines with strict integration +- Security-regulated environments (IoT, medical, fintech) +- Package maintainers enforcing internal boundaries + +--- + +## 💡 Why It Matters + +Python’s flexibility comes at a cost: + +- Silent runtime mismatches +- Missing methods or classes +- Platform-dependent failures +- No enforcement over module consumers + +**ImportSpy brings governance** +to how, when, and where modules are imported. + +--- + +## ❤️ Contribute & Support + +- ⭐ [Star on GitHub](https://github.com/atellaluca/ImportSpy) +- 🐛 [File issues or feature requests](https://github.com/atellaluca/ImportSpy/issues) +- 🤝 [Contribute](https://github.com/atellaluca/ImportSpy/blob/main/CONTRIBUTING.md) +- 💖 [Sponsor on GitHub](https://github.com/sponsors/atellaluca) + +--- + +## 📜 License + +MIT © 2024 – Luca Atella +![ImportSpy logo](./assets/importspy-logo.png) diff --git a/README.rst b/README.rst deleted file mode 100644 index 2b554f8..0000000 --- a/README.rst +++ /dev/null @@ -1,190 +0,0 @@ -.. image:: https://img.shields.io/github/license/atellaluca/importspy - :alt: License - -.. image:: https://img.shields.io/pypi/v/importspy - :target: https://pypi.org/project/importspy/ - :alt: PyPI Version - -.. image:: https://img.shields.io/pypi/pyversions/importspy - :alt: Supported Python Versions - -.. image:: https://img.shields.io/github/actions/workflow/status/atellaluca/ImportSpy/python-package.yml - :target: https://github.com/atellaluca/ImportSpy/actions/workflows/python-package.yml - :alt: Build Status - -.. image:: https://img.shields.io/readthedocs/importspy - :target: https://importspy.readthedocs.io/ - :alt: Documentation Status - -.. image:: https://github.com/atellaluca/ImportSpy/blob/main/assets/importspy-banner.png - :alt: ImportSpy – Runtime Contract Validation for Python - :width: 500px - -ImportSpy -========= - -Contract-based import validation for Python modules. - -*Runtime-safe, structure-aware, declarative.* - -ImportSpy allows your Python modules to define explicit **import contracts**: -rules about where, how, and by whom they can be safely imported — and blocks any import that doesn’t comply. - -🔍 Key Benefits ---------------- - -- ✅ Prevent import from unsupported environments -- ✅ Enforce structural expectations (classes, attributes, arguments) -- ✅ Control who can use your module and how -- ✅ Reduce runtime surprises across CI, staging, and production -- ✅ Define everything in readable `.yml` contracts - -💡 Why ImportSpy? ------------------ - -Python is flexible, but uncontrolled imports can lead to: - -- 🔥 Silent runtime failures -- 🔍 Structural mismatches (wrong or missing methods/classes) -- 🌍 Inconsistent behavior across platforms -- 🚫 Unauthorized usage of internal code - -ImportSpy offers you **runtime import governance** — clearly defined, enforced in real-time. - -📐 Architecture Highlight -------------------------- - -.. image:: https://raw.githubusercontent.com/atellaluca/ImportSpy/refs/heads/main/assets/importspy-spy-model-architecture.png - :alt: ImportSpy, SpyModel Architecture - :width: 830 - -ImportSpy uses a layered model (`SpyModel`) that mirrors your execution context and module structure: - -- `Runtime` → defines architecture and system -- `System` → declares OS and environment variables -- `Python` → specifies interpreter, version, and modules -- `Module` → lists classes, functions, variables (each represented as objects, not dicts) - -Each element is introspected and validated dynamically, at runtime or via CLI. - -📜 Contract Example -------------------- - -.. code-block:: yaml - - filename: plugin.py - variables: - - name: mode - value: production - annotation: str - classes: - - name: Plugin - methods: - - name: run - arguments: - - name: self - - name: data - annotation: dict - return_annotation: None - -📦 Installation ---------------- - -.. code-block:: bash - - pip install importspy - -✅ Requires Python 3.10+ - -🔒 Usage Modes --------------- - -**Embedded Mode** – the module protects itself: - -.. image:: https://raw.githubusercontent.com/atellaluca/ImportSpy/refs/heads/main/assets/importspy-embedded-mode.png - :alt: How ImportSpy Embedded Mode Works - :width: 830 - -.. code-block:: python - - from importspy import Spy - importer = Spy().importspy(filepath="spymodel.yml") - importer.Plugin().run() - -**CLI Mode** – validate externally in CI/CD: - -.. image:: https://raw.githubusercontent.com/atellaluca/ImportSpy/refs/heads/main/assets/importspy-works.png - :alt: How ImportSpy CLI Mode Works - :width: 830 - -.. code-block:: bash - - importspy -s spymodel.yml -l DEBUG path/to/module.py - -📚 Features Overview --------------------- - -- ✅ Runtime validation based on import contracts -- ✅ YAML-based, declarative format -- ✅ Fine-grained introspection of classes, functions, arguments -- ✅ OS, architecture, interpreter matching -- ✅ Full error messages, CI-friendly output -- ✅ Supports embedded or external enforcement -- ✅ Strong internal model (`SpyModel`) powered by `pydantic` - -🚀 Ideal Use Cases ------------------- - -- 🛡️ Security-sensitive systems (finance, IoT, medical) -- 🧩 Plugin-based architectures (CMS, CLI, extensions) -- 🧪 CI/CD pipelines with strict integration rules -- 🧱 Frameworks with third-party extension points -- 📦 Package maintainers enforcing integration rules - -🧠 How It Works ---------------- - -1. Define your contract in `.yml` or Python. -2. ImportSpy loads your module and introspects its importer. -3. Runtime environment + structure are matched against the contract. -4. If mismatch → import blocked. - If valid → import continues safely. - -🎯 Tech Stack -------------- - -- ✅ Pydantic 2.x – contract validation engine -- ✅ Typer – CLI interface -- ✅ ruamel.yaml – YAML parsing -- ✅ inspect + sys – runtime context introspection -- ✅ Poetry – package + dependency management -- ✅ Sphinx + ReadTheDocs – full docs and architecture reference - -📘 Documentation ----------------- - -- 🔗 Full Docs → https://importspy.readthedocs.io/ -- 🧱 Model Overview → https://importspy.readthedocs.io/en/latest/advanced/architecture_index.html -- 🧪 Use Cases → https://importspy.readthedocs.io/en/latest/overview/use_cases_index.html - -🌟 Contribute & Support ------------------------ - -- ⭐ Star → https://github.com/atellaluca/ImportSpy -- 🛠 Contribute via issues or PRs -- 💖 Sponsor → https://github.com/sponsors/atellaluca - -🔥 **Let your modules enforce their own rules.** -Start importing with structure. - -📜 License ----------- - -MIT © 2024 – Luca Atella - -.. image:: ./assets/importspy-logo.png - :alt: ImportSpy Logo - :width: 100px - :align: center - -**ImportSpy** is an open-source project maintained with ❤️ by `Luca Atella `_. From cbe4a78c6be8e8b39856bc6c3acda78ad484403f Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Sat, 2 Aug 2025 19:35:26 +0200 Subject: [PATCH 32/40] docs: move contributing guidelines to project root and link from mkdocs --- CONTRIBUTING.md | 142 +++++++++++++++++++++++++++++++++++++-------- docs/contribute.md | 0 mkdocs.yml | 2 +- 3 files changed, 119 insertions(+), 25 deletions(-) delete mode 100644 docs/contribute.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9b16053..b3d2025 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,33 +1,127 @@ # Contributing to ImportSpy -🎉 Thank you for considering a contribution to **ImportSpy**! We value your efforts and welcome **issues, features, and documentation improvements**. +🎉 Thank you for considering a contribution to **ImportSpy** — a project designed to bring structure, security, and clarity to Python imports. + +We welcome all kinds of contributions: new features, bug fixes, tests, documentation, and even thoughtful ideas. Your involvement makes the project better. + +--- ## 📢 How to Contribute -### 🔍 1. Issue Reporting -If you find a **bug** or have a **feature request**, please open an issue: -🔗 [GitHub Issues](https://github.com/atellaluca/ImportSpy/issues) - -### 🔄 2. Fork & Branching Strategy -- **Create a fork** and work on a separate branch before submitting a pull request. -- Use the following **branch naming conventions**: - - **For features:** `feature/nome-feature-in-breve` - - **For bug fixes:** `fix/bugfix-description` - - **For documentation:** `docs/update-section` - -### ✅ 3. Code Style & Commit Rules -- **Follow Conventional Commits**: - - `feat:` → For new features - - `fix:` → For bug fixes - - `docs:` → For documentation changes - - `test:` → For tests - - `refactor:` → Code improvements without changing functionality - -Example: +### 1. Open an Issue +If you encounter a bug, have a feature suggestion, or want to propose a change, please [open an issue](https://github.com/atellaluca/ImportSpy/issues). Use clear and descriptive titles. + +You can use labels such as: +- `bug`: for broken functionality +- `enhancement`: for feature requests +- `question`: for clarification or design discussions + +### 2. Fork and Branch Strategy + +- Create a **fork** of the repository. +- Work in a dedicated **feature branch** off `main`. +- Use consistent branch naming: + +| Purpose | Branch Name Format | +|----------------|-------------------------------| +| Feature | `feature/short-description` | +| Bug fix | `fix/issue-description` | +| Documentation | `docs/section-description` | +| Tests | `test/feature-or-bug-name` | + +> 💡 Keep pull requests focused and small. This helps reviewers and speeds up merging. + +--- + +## ✅ Code Standards + +### Python Style Guide +ImportSpy follows: +- [PEP8](https://peps.python.org/pep-0008/) +- Type hints throughout the codebase +- `black` + `ruff` for formatting and linting +- `pydantic` for data models + +### Linting & Tests + +To run tests and lint checks: + ```bash -git commit -m "feat: add validation for OS compatibility" +poetry install +pytest +ruff check . +black --check . ``` -💡 **We Appreciate Every Contribution!** +--- + +## 📄 Commit Conventions + +We use **[Conventional Commits](https://www.conventionalcommits.org/)** for readable history and automatic changelog generation. + +| Type | Use For | +|----------|-------------------------------| +| `feat:` | New feature | +| `fix:` | Bug fix | +| `docs:` | Documentation only | +| `refactor:` | Code change w/o new feature or fix | +| `test:` | Adding or updating tests | +| `chore:` | Internal tooling or CI | + +**Example:** +```bash +git commit -m "feat: support multiple Python interpreters in contract" +``` + +--- + +## 🧪 Test Philosophy + +Tests live in `tests/validators/` and should: +- Cover core logic (validators, spymodel, contract violations) +- Include both positive and negative cases +- Be fast, deterministic, and isolated + +--- + +## ✍️ Docs Contributions + +We use **MkDocs + Material** for documentation. Docs live under: + +``` +docs/ +``` + +To preview locally: + +```bash +poetry install +mkdocs serve +``` + +New pages should be added to `mkdocs.yml` under the right section. + +--- + +## 🙌 Join the Community + +While we don’t yet have a Discord or forum, we encourage: +- Sharing feedback via GitHub Issues +- Discussing architecture via PRs and comments +- Connecting with the maintainer via LinkedIn or GitHub + +--- + +## 💬 Need Help? + +Open an issue with the `question` label or ping @atellaluca in your PR. + +--- + +## 📜 License + +By contributing, you agree your work will be released under the MIT License. + +--- -ImportSpy is an open-source project, and every contribution—big or small—helps make it better. 🚀 \ No newline at end of file +**Let your modules enforce their own rules — and thank you for helping ImportSpy grow!** diff --git a/docs/contribute.md b/docs/contribute.md deleted file mode 100644 index e69de29..0000000 diff --git a/mkdocs.yml b/mkdocs.yml index 6722008..9dc7965 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -33,7 +33,7 @@ nav: - Violation System: advanced/violations.md - Validation & Errors: validation-errors.md - API Reference: api-reference.md - - Contributing: contribute.md + - Contributing: ../CONTRIBUTING.md plugins: - search From 8246444744d65c21866ba60a00e89b72d3d6b54b Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Mon, 4 Aug 2025 19:17:40 +0200 Subject: [PATCH 33/40] docs: rewrite contract syntax page and update validation path --- docs/contracts/examples.md | 168 ++++++++++++++ docs/contracts/syntax.md | 214 ++++++++++++++++++ docs/errors/contract-violations.md | 85 +++++++ docs/errors/contract_violations.md | 24 -- .../errors/{error_table.md => error-table.md} | 0 mkdocs.yml | 5 +- 6 files changed, 469 insertions(+), 27 deletions(-) create mode 100644 docs/contracts/examples.md create mode 100644 docs/errors/contract-violations.md delete mode 100644 docs/errors/contract_violations.md rename docs/errors/{error_table.md => error-table.md} (100%) diff --git a/docs/contracts/examples.md b/docs/contracts/examples.md new file mode 100644 index 0000000..393dede --- /dev/null +++ b/docs/contracts/examples.md @@ -0,0 +1,168 @@ +# Contract Examples + +This page provides complete examples of import contracts (`.yml` files) supported by ImportSpy. + +Each example demonstrates how to declare structural expectations and runtime constraints for a Python module using the SpyModel format. + +--- + +## Basic Module Contract + +This example defines a simple module called `plugin.py` with one variable, one class, and one method. + +```yaml +filename: plugin.py +variables: + - name: mode + value: production + annotation: str +classes: + - name: Plugin + methods: + - name: run + arguments: + - name: self + return_annotation: None +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.11 + interpreter: CPython +``` + +--- + +## Function With Typed Arguments + +This contract describes a module where the `analyze` function requires two arguments with specific types. + +```yaml +filename: analyzer.py +functions: + - name: analyze + arguments: + - name: self + - name: data + annotation: list[str] + - name: verbose + annotation: bool + return_annotation: dict +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.12 + interpreter: CPython +``` + +--- + +## Class With Attributes and Methods + +This contract defines a module that exposes a `TaskManager` class with attributes and a method. + +```yaml +filename: manager.py +classes: + - name: TaskManager + attributes: + - name: tasks + annotation: list[str] + - name: state + annotation: str + methods: + - name: reset + arguments: + - name: self + return_annotation: None +deployments: + - arch: arm64 + systems: + - os: darwin + environment: + variables: + - name: MODE + value: development + annotation: str + pythons: + - version: 3.11 + interpreter: CPython +``` + +--- + +## Runtime-Only Validation + +This contract enforces only environmental and interpreter constraints — no structural validation. + +```yaml +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.10 + interpreter: CPython +``` + +--- + +## Multiple Deployments + +If your module supports multiple platforms, you can define multiple `deployments`. + +```yaml +filename: plugin.py +classes: + - name: Plugin + methods: + - name: run + arguments: + - name: self +deployments: + - arch: x86_64 + systems: + - os: linux + pythons: + - version: 3.11 + interpreter: CPython + - arch: arm64 + systems: + - os: darwin + pythons: + - version: 3.12 + interpreter: CPython +``` + +--- + +## Using Environment Variables + +This example enforces that an environment variable is set and has a given type. + +```yaml +filename: checker.py +deployments: + - arch: x86_64 + systems: + - os: linux + environment: + variables: + - name: DEBUG + value: "true" + annotation: str + pythons: + - version: 3.10 + interpreter: CPython +``` + +--- + +## Related Topics + +- [Contract Syntax](syntax.md) +- [Violation System](../advanced/violations.md) +- [SpyModel Architecture](../advanced/spymodel.md) diff --git a/docs/contracts/syntax.md b/docs/contracts/syntax.md index e69de29..982fde5 100644 --- a/docs/contracts/syntax.md +++ b/docs/contracts/syntax.md @@ -0,0 +1,214 @@ +# Contract Syntax + +ImportSpy contracts are written in YAML and describe the **structure and runtime expectations** of a Python module. These contracts can be embedded or validated externally (e.g., in CI/CD), and act as **import-time filters** for enforcing compatibility and intent. + +A contract defines: +- What variables, classes, and functions a module must expose +- What runtime conditions are required (OS, architecture, Python version, etc.) +- How strict or flexible the structure must be + +This document explains the full syntax supported in `.yml` contract files. + +--- + +## Top-Level Structure + +Every `.yml` contract is structured as follows: + +```yaml +filename: plugin.py +version: "1.2.3" + +variables: + - name: MODE + value: production + annotation: str + +functions: + - name: initialize + arguments: + - name: self + - name: config + annotation: dict + return_annotation: None + +classes: + - name: Plugin + attributes: + - name: settings + value: default + annotation: dict + type: instance + methods: + - name: run + arguments: + - name: self + return_annotation: None + superclasses: + - BasePlugin + +deployments: + - arch: x86_64 + systems: + - os: linux + environment: + variables: + - name: IMPORTSPY_ENABLED + value: true + annotation: bool + pythons: + - version: "3.11" + interpreter: CPython + modules: + - filename: plugin.py + version: "1.2.3" +``` + +--- + +## Fields Explained + +### `filename` +The name of the target Python file or module this contract applies to. + +### `version` +Optional string that defines the expected version of the module. Can be used to pin specific builds or releases. + +--- + +## `variables` + +Declares **global or module-level variables** the importer must provide. + +```yaml +variables: + - name: DEBUG + value: true + annotation: bool +``` + +Each variable entry supports: +- `name` (**required**): Variable name +- `value` (optional): Expected value +- `annotation` (optional): Expected type annotation as string (e.g., `"str"`, `"dict"`) + +--- + +## `functions` + +Specifies required functions in the importing module. + +```yaml +functions: + - name: load_config + arguments: + - name: path + annotation: str + return_annotation: dict +``` + +Each function supports: +- `name`: The function's name +- `arguments`: List of required arguments (each with optional `annotation`) +- `return_annotation`: Expected return type (as string) + +--- + +## `classes` + +Defines required class structures. + +```yaml +classes: + - name: Plugin + attributes: + - name: config + annotation: dict + value: {} + type: instance + methods: + - name: execute + arguments: + - name: self + superclasses: + - BasePlugin +``` + +A class may include: +- `name`: Class name +- `attributes`: A list of attributes exposed by the class + - `type`: Can be `"instance"` or `"class"` to indicate attribute level +- `methods`: Required method declarations (same format as top-level `functions`) +- `superclasses`: Optional list of superclass names expected + +> Attributes are matched on name, annotation, and (if provided) value. + +--- + +## `deployments` + +This section defines **runtime constraints** in which the import is valid. It allows validation based on: + +- Architecture +- Operating system +- Environment variables +- Python version and interpreter +- Declared modules + +```yaml +deployments: + - arch: x86_64 + systems: + - os: linux + environment: + variables: + - name: MODE + value: prod + annotation: str + pythons: + - version: "3.10" + interpreter: CPython + modules: + - filename: plugin.py +``` + +### Deployment fields: + +| Field | Description | +|--------------|---------------------------------------------| +| `arch` | CPU architecture (e.g. `x86_64`, `arm64`) | +| `systems.os` | Operating system (e.g. `linux`, `windows`) | +| `environment.variables` | Required runtime env variables | +| `pythons.version` | Required Python version (string) | +| `pythons.interpreter` | Interpreter (e.g., `CPython`) | +| `modules` | Specific modules to check (with filename/version) | + +> Note: all conditions are **AND-combined** within a deployment block. + +--- + +## Matching Rules + +ImportSpy uses a strict structural validator. Here are some notes: + +- Variables, functions, and methods are matched **by name**. +- Annotations are matched **as plain strings** – no semantic typing or runtime evaluation. +- If an annotation is omitted, it is **not enforced**. +- Superclasses are checked only **by name**, not by inheritance tree resolution. + +--- + +## Best Practices + +- Use consistent annotations: `"str"`, `"dict"`, `"list"`, etc. +- Prefer matching exact function signatures for critical plugins +- Define environment constraints only when needed (e.g., `IMPORTSPY_MODE=prod`) +- Use `modules.filename` to enforce versioning in multi-plugin systems + +--- + +## Related Sections + +- [Contract Examples](examples.md) +- [SpyModel Architecture](../advanced/spymodel.md) +- [Contract Violations](../validation-errors.md) diff --git a/docs/errors/contract-violations.md b/docs/errors/contract-violations.md new file mode 100644 index 0000000..3d6c308 --- /dev/null +++ b/docs/errors/contract-violations.md @@ -0,0 +1,85 @@ +# Contract Violations + +When an import contract is not satisfied, **ImportSpy** blocks the import and raises a detailed error message. +These violations are central to the library's purpose: enforcing predictable, secure, and valid module usage across Python runtimes. + +## How Violations Work + +Every time a module is imported using ImportSpy (either in **embedded** or **CLI** mode), the system performs deep introspection and validation checks. + +If something does not match the declared contract (`.yml`), ImportSpy will: + +1. **Capture the context** (e.g., `MODULE`, `CLASS`, `RUNTIME`, etc.) +2. **Identify the type** of error: + - `missing`: required element is absent + - `mismatch`: expected vs actual values differ + - `invalid`: unexpected or disallowed value found +3. **Generate a structured error message** including: + - a human-readable message + - exact label of the failing entity + - possible solutions or corrective actions + +These violations are raised as `ValueError`, but contain detailed introspection metadata under the hood. + +--- + +## Error Categories + +ImportSpy organizes violations into **logical layers**, based on what is being validated: + +| Layer | Validator Class | Violation Raised | +|-------------------|--------------------------|------------------------------------------| +| Architecture/OS | `RuntimeValidator` | `RuntimeContractViolation` | +| OS / Environment | `SystemValidator` | `SystemContractViolation` | +| Python Interpreter| `PythonValidator` | `PythonContractViolation` | +| Module File | `ModuleValidator` | `ModuleContractViolation` | +| Class Structure | `ClassValidator` | `ModuleContractViolation (CLASS_CONTEXT)` | +| Functions | `FunctionValidator` | `FunctionContractViolation` | +| Variables / Args | `VariableValidator` | `VariableContractViolation` | + +Each of these violations inherits from `BaseContractViolation`, which provides: +- A consistent interface for labeling (`.label()`) +- Templated messages for each category +- A `Bundle` object used to inject dynamic context into the error + +--- + +## Error Message Anatomy + +A full ImportSpy violation message looks like this: + +``` +[MODULE] Expected variable `timeout: int` not found in `my_module.py` +→ Please add the variable or update your contract. +``` + +Each message consists of: +- `[CONTEXT]`: tells where the error occurred +- **Label**: dynamically generated from the contract structure +- **Expected/Actual**: shown for mismatch/invalid errors +- **Solution**: human-readable advice from the YAML spec + +--- + +## Debugging Tips + +- Use `-l DEBUG` when invoking ImportSpy via CLI to see exact comparison steps. +- Violations are deterministic and reproducible. If one fails in CI, it will fail locally too. +- You can inspect the violation context by capturing the `ValueError` and logging its message. + +--- + +## 📋 Contract Violation Table + +Below is a comprehensive list of all possible error messages emitted by ImportSpy: + +--8<-- "errors/error_table.md" + +--- + +## Related Topics + +- [Contract Syntax](../contracts/syntax.md) +- [Embedded Mode](../modes/embedded.md) +- [CLI Mode](../modes/cli.md) +- [SpyModel Architecture](../advanced/spymodel.md) diff --git a/docs/errors/contract_violations.md b/docs/errors/contract_violations.md deleted file mode 100644 index 4eec19f..0000000 --- a/docs/errors/contract_violations.md +++ /dev/null @@ -1,24 +0,0 @@ -# Contract Violations - -ImportSpy raises clear and structured errors when an import contract is not satisfied. - -Each error message includes: -- The **context** of the violation (runtime, environment, module, class) -- The **type** of error (missing, mismatch, invalid) -- A **precise description** of the problem and how to fix it - -Below is a list of possible contract violation messages you may encounter. - ---- - -## 📋 Error Messages - ---8<-- "errors/error_table.md" - ---- - -## 🔗 Related Topics - -- [Contract Syntax](../contracts/syntax.md) -- [Embedded Mode](../modes/embedded.md) -- [CLI Mode](../modes/cli.md) diff --git a/docs/errors/error_table.md b/docs/errors/error-table.md similarity index 100% rename from docs/errors/error_table.md rename to docs/errors/error-table.md diff --git a/mkdocs.yml b/mkdocs.yml index 9dc7965..f17dda3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -27,11 +27,10 @@ nav: - Syntax: contracts/syntax.md - Examples: contracts/examples.md - Advanced Usage: - - CI/CD Integration: advanced/cicd.md - - Plugin Systems: advanced/plugins.md +# - CI/CD Integration: advanced/cicd.md - SpyModel Architecture: advanced/spymodel.md - Violation System: advanced/violations.md - - Validation & Errors: validation-errors.md + - Validation & Errors: errors/contract-violations.md - API Reference: api-reference.md - Contributing: ../CONTRIBUTING.md From 02d5a9fac22e771367ff0690e2b4c4057544619d Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Wed, 6 Aug 2025 18:17:22 +0200 Subject: [PATCH 34/40] docs(uml): update UML diagrams --- assets/importspy-spy-model-architecture.png | Bin 122599 -> 135067 bytes .../importspy-spy-model-architecture.drawio | 428 ++++++++++++++++++ 2 files changed, 428 insertions(+) create mode 100644 diagrams/importspy-spy-model-architecture.drawio diff --git a/assets/importspy-spy-model-architecture.png b/assets/importspy-spy-model-architecture.png index f40bb256d3ed7c950f0138f3209d77bb6d1dde89..35b8a46e4a0d270bfafa1aa664d1a0d93cac7998 100644 GIT binary patch literal 135067 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

+H<;2kRttpph@ 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 G3HcvRtSZI; literal 122599 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 27fc773be5c994d698012f9f4872c43d3b3fb0d6 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Wed, 6 Aug 2025 18:20:42 +0200 Subject: [PATCH 35/40] chore: bumping to 0.4.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fcd1e92..f510657 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "importspy" -version = "0.3.3" +version = "0.4.0" description = "ImportSpy ensures structural integrity, runtime compliance, and security for external modules, preventing inconsistencies and enforcing controlled execution." license = "MIT" authors = ["Luca Atella "] From 125d09075b20d25b8a035e2fdc87c703a8b4ab02 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Wed, 6 Aug 2025 18:26:44 +0200 Subject: [PATCH 36/40] chore: add /site/ on .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 28b18d0..ef3447e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ venv/ ENV/ env.bak/ venv.bak/ -.pytest_cache/ \ No newline at end of file +.pytest_cache/ +/site/ \ No newline at end of file From a626c8c70fe97f232f869dc1c41c4cb5921edfb1 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Wed, 6 Aug 2025 18:50:32 +0200 Subject: [PATCH 37/40] docs: clean homepage and unify contributing path --- docs/index.md | 7 --- mkdocs.yml | 3 +- poetry.lock | 114 ++++++++++++++++++++++++++++++++++++++++--------- pyproject.toml | 5 ++- 4 files changed, 99 insertions(+), 30 deletions(-) diff --git a/docs/index.md b/docs/index.md index 3904009..dce7439 100644 --- a/docs/index.md +++ b/docs/index.md @@ -105,12 +105,6 @@ ImportSpy is built around 3 key components: - [API Docs](api-reference.md) -### 🤝 Contributing - -- [Contributing Guidelines](../CONTRIBUTING.md) -- [Security Policy](../SECURITY.md) -- [License (MIT)](../LICENSE) - --- ## Architecture Diagram @@ -139,7 +133,6 @@ If ImportSpy is useful in your infrastructure, help us grow by: - [Starring the project on GitHub](https://github.com/your-org/importspy) - [Becoming a GitHub Sponsor](https://github.com/sponsors/your-org) -- [Contributing modules, tests, or docs](../CONTRIBUTING.md) --- diff --git a/mkdocs.yml b/mkdocs.yml index f17dda3..87e3902 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -32,7 +32,7 @@ nav: - Violation System: advanced/violations.md - Validation & Errors: errors/contract-violations.md - API Reference: api-reference.md - - Contributing: ../CONTRIBUTING.md + - Contributing: CONTRIBUTING.md plugins: - search @@ -48,4 +48,3 @@ markdown_extensions: - pymdownx.tabbed: alternate_style: true - diff --git a/poetry.lock b/poetry.lock index 4cd2ed6..a9a240f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -18,7 +18,7 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -38,7 +38,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} +markers = {main = "platform_system == \"Windows\""} [[package]] name = "exceptiongroup" @@ -65,7 +65,7 @@ version = "2.1.0" description = "Copy your docs directly to the gh-pages branch." optional = false python-versions = "*" -groups = ["main"] +groups = ["dev"] files = [ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, @@ -77,6 +77,21 @@ python-dateutil = ">=2.8.1" [package.extras] dev = ["flake8", "markdown", "twine", "wheel"] +[[package]] +name = "griffe" +version = "1.10.0" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "griffe-1.10.0-py3-none-any.whl", hash = "sha256:a5eec6d5431cc49eb636b8a078d2409844453c1b0e556e4ba26f8c923047cd11"}, + {file = "griffe-1.10.0.tar.gz", hash = "sha256:7fe89ebfb5140e0589748888b99680968e5b9ef7e2dcb2b01caf87ec552b66be"}, +] + +[package.dependencies] +colorama = ">=0.4" + [[package]] name = "iniconfig" version = "2.1.0" @@ -95,7 +110,7 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["dev"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -113,7 +128,7 @@ version = "3.8.2" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24"}, {file = "markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45"}, @@ -154,7 +169,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -237,7 +252,7 @@ version = "1.3.4" description = "A deep merge function for 🐍." optional = false python-versions = ">=3.6" -groups = ["main"] +groups = ["dev"] files = [ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, @@ -249,7 +264,7 @@ version = "1.6.1" description = "Project documentation with Markdown." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, @@ -274,13 +289,30 @@ watchdog = ">=2.0" i18n = ["babel (>=2.9.0)"] min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] +[[package]] +name = "mkdocs-autorefs" +version = "1.4.2" +description = "Automatically link across pages in MkDocs." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13"}, + {file = "mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749"}, +] + +[package.dependencies] +Markdown = ">=3.3" +markupsafe = ">=2.0.1" +mkdocs = ">=1.1" + [[package]] name = "mkdocs-get-deps" version = "0.2.0" description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, @@ -291,13 +323,57 @@ mergedeep = ">=1.3.4" platformdirs = ">=2.2.0" pyyaml = ">=5.1" +[[package]] +name = "mkdocstrings" +version = "0.30.0" +description = "Automatic documentation from sources, for MkDocs." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mkdocstrings-0.30.0-py3-none-any.whl", hash = "sha256:ae9e4a0d8c1789697ac776f2e034e2ddd71054ae1cf2c2bb1433ccfd07c226f2"}, + {file = "mkdocstrings-0.30.0.tar.gz", hash = "sha256:5d8019b9c31ddacd780b6784ffcdd6f21c408f34c0bd1103b5351d609d5b4444"}, +] + +[package.dependencies] +Jinja2 = ">=2.11.1" +Markdown = ">=3.6" +MarkupSafe = ">=1.1" +mkdocs = ">=1.6" +mkdocs-autorefs = ">=1.4" +mkdocstrings-python = {version = ">=1.16.2", optional = true, markers = "extra == \"python\""} +pymdown-extensions = ">=6.3" + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=1.16.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "1.16.12" +description = "A Python handler for mkdocstrings." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "mkdocstrings_python-1.16.12-py3-none-any.whl", hash = "sha256:22ded3a63b3d823d57457a70ff9860d5a4de9e8b1e482876fc9baabaf6f5f374"}, + {file = "mkdocstrings_python-1.16.12.tar.gz", hash = "sha256:9b9eaa066e0024342d433e332a41095c4e429937024945fea511afe58f63175d"}, +] + +[package.dependencies] +griffe = ">=1.6.2" +mkdocs-autorefs = ">=1.4" +mkdocstrings = ">=0.28.3" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + [[package]] name = "packaging" version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, @@ -309,7 +385,7 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -321,7 +397,7 @@ version = "4.3.8" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, @@ -503,7 +579,7 @@ version = "10.16.1" description = "Extension pack for Python Markdown." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d"}, {file = "pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91"}, @@ -546,7 +622,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] +groups = ["dev"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -561,7 +637,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -624,7 +700,7 @@ version = "1.1" description = "A custom YAML tag for referencing environment variables in YAML files." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"}, {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"}, @@ -747,7 +823,7 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] +groups = ["dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -848,7 +924,7 @@ version = "6.0.0" description = "Filesystem events monitoring" optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["dev"] files = [ {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, @@ -888,4 +964,4 @@ watchmedo = ["PyYAML (>=3.10)"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "32f9d75cf07ddd3bdd46f748776a754bf4e718de7e1331aeb9c93002bd2ea451" +content-hash = "4518c79cecbdff96f897154506a0a0183050a76ac4dda54278518f4629690f58" diff --git a/pyproject.toml b/pyproject.toml index f510657..c18b327 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "0.4.0" description = "ImportSpy ensures structural integrity, runtime compliance, and security for external modules, preventing inconsistencies and enforcing controlled execution." license = "MIT" authors = ["Luca Atella "] -readme = "README.rst" +readme = "README.md" packages = [{include = "importspy", from = "src"}] [tool.poetry.dependencies] @@ -12,12 +12,13 @@ python = "^3.10" pydantic = "^2.9.2" ruamel-yaml = "^0.18.10" typer = "^0.15.2" -mkdocs = "^1.6.1" pymdown-extensions = "^10.16.1" [tool.poetry.group.dev.dependencies] pytest = "^8.3.3" +mkdocs = "^1.6.1" +mkdocstrings = {version = "^0.30.0", extras = ["python"]} [tool.poetry.urls] Repository = "https://github.com/atellaluca/importspy" From f5a609bd4641c4ed0f35a03eb21545cb4533b5ee Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Thu, 7 Aug 2025 16:33:12 +0200 Subject: [PATCH 38/40] chore(dependencies): updating rich 14.0.0 -> 14.1.0 --- poetry.lock | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index a9a240f..92785f1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -711,20 +711,19 @@ pyyaml = "*" [[package]] name = "rich" -version = "14.0.0" +version = "14.1.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" groups = ["main"] files = [ - {file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"}, - {file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"}, + {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, + {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] From d7b7bc0cccb7804c9c0158c185d2714936adfa6e Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Thu, 7 Aug 2025 20:01:43 +0200 Subject: [PATCH 39/40] chore(docs): cleanup assets and update links, unify utility docstrings --- docs/assets/apple-touch-icon.png | Bin 0 -> 11539 bytes docs/assets/favicon.ico | Bin 0 -> 32038 bytes {assets => docs/assets}/importspy-banner.png | Bin .../assets}/importspy-embedded-mode.png | Bin {assets => docs/assets}/importspy-logo.png | Bin .../importspy-spy-model-architecture.png | Bin {assets => docs/assets}/importspy-works.png | Bin docs/contracts/syntax.md | 2 +- docs/index.md | 6 +- docs/modes/cli.md | 10 +- docs/modes/embedded.md | 5 +- docs/overrides/extra.html | 1 + docs/use_cases/index.md | 4 +- mkdocs.yml | 4 +- src/importspy/utilities/__init__.py | 0 src/importspy/utilities/module_util.py | 183 ++++++++++-------- src/importspy/utilities/python_util.py | 81 ++++---- src/importspy/utilities/runtime_util.py | 62 +++--- src/importspy/utilities/system_util.py | 93 ++++----- 19 files changed, 227 insertions(+), 224 deletions(-) create mode 100644 docs/assets/apple-touch-icon.png create mode 100644 docs/assets/favicon.ico rename {assets => docs/assets}/importspy-banner.png (100%) rename {assets => docs/assets}/importspy-embedded-mode.png (100%) rename {assets => docs/assets}/importspy-logo.png (100%) rename {assets => docs/assets}/importspy-spy-model-architecture.png (100%) rename {assets => docs/assets}/importspy-works.png (100%) create mode 100644 docs/overrides/extra.html create mode 100644 src/importspy/utilities/__init__.py diff --git a/docs/assets/apple-touch-icon.png b/docs/assets/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..98a6468581c0612b4e0b472565b57285d2a43ce5 GIT binary patch literal 11539 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 literal 0 HcmV?d00001 diff --git a/assets/importspy-banner.png b/docs/assets/importspy-banner.png similarity index 100% rename from assets/importspy-banner.png rename to docs/assets/importspy-banner.png diff --git a/assets/importspy-embedded-mode.png b/docs/assets/importspy-embedded-mode.png similarity index 100% rename from assets/importspy-embedded-mode.png rename to docs/assets/importspy-embedded-mode.png diff --git a/assets/importspy-logo.png b/docs/assets/importspy-logo.png similarity index 100% rename from assets/importspy-logo.png rename to docs/assets/importspy-logo.png diff --git a/assets/importspy-spy-model-architecture.png b/docs/assets/importspy-spy-model-architecture.png similarity index 100% rename from assets/importspy-spy-model-architecture.png rename to docs/assets/importspy-spy-model-architecture.png diff --git a/assets/importspy-works.png b/docs/assets/importspy-works.png similarity index 100% rename from assets/importspy-works.png rename to docs/assets/importspy-works.png diff --git a/docs/contracts/syntax.md b/docs/contracts/syntax.md index 982fde5..ce65c89 100644 --- a/docs/contracts/syntax.md +++ b/docs/contracts/syntax.md @@ -211,4 +211,4 @@ ImportSpy uses a strict structural validator. Here are some notes: - [Contract Examples](examples.md) - [SpyModel Architecture](../advanced/spymodel.md) -- [Contract Violations](../validation-errors.md) +- [Contract Violations](../errors/contract-violations.md) diff --git a/docs/index.md b/docs/index.md index dce7439..44540e4 100644 --- a/docs/index.md +++ b/docs/index.md @@ -94,8 +94,8 @@ ImportSpy is built around 3 key components: ### 🧠 Validation Engine - [Violation System](advanced/violations.md) -- [Contract Violations](errors/contract_violations.md) -- [Error Table](errors/error_table.md) +- [Contract Violations](errors/contract-violations.md) +- [Error Table](errors/error-table.md) ### 📦 Use Cases @@ -109,7 +109,7 @@ ImportSpy is built around 3 key components: ## Architecture Diagram -![SpyModel UML](../assets/importspy-spy-model-architecture.png) +![SpyModel UML](assets/importspy-spy-model-architecture.png) --- diff --git a/docs/modes/cli.md b/docs/modes/cli.md index 254c3f5..25515e7 100644 --- a/docs/modes/cli.md +++ b/docs/modes/cli.md @@ -95,11 +95,6 @@ If it doesn't, you’ll see a structured error like: --- -## Learn more - -- [→ Embedded Mode](embedded.md) -- [→ Contract syntax](../contracts/syntax.md) -- [→ Error types](../errors/index.md) # Import Contract Syntax An ImportSpy contract is a YAML file that describes: @@ -277,6 +272,5 @@ environment: ## Learn more -- [Embedded Mode](../modes/embedded.md) -- [CLI Mode](../modes/cli.md) -- [Contract violations](../errors/index.md) +- [Contract syntax](../contracts/syntax.md) +- [Contract violations](../errors/contract-violations.md) diff --git a/docs/modes/embedded.md b/docs/modes/embedded.md index 83091a6..c5a2d88 100644 --- a/docs/modes/embedded.md +++ b/docs/modes/embedded.md @@ -70,6 +70,5 @@ Use this mode when: ## Learn more -- [→ CLI Mode](cli.md) -- [→ Contract syntax](../contracts/syntax.md) -- [→ Error types](../errors/index.md) +- [Contract syntax](../contracts/syntax.md) +- [Contract violations](../errors/contract-violations.md) diff --git a/docs/overrides/extra.html b/docs/overrides/extra.html new file mode 100644 index 0000000..d9dfd47 --- /dev/null +++ b/docs/overrides/extra.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/use_cases/index.md b/docs/use_cases/index.md index 0f18c3d..f72cf82 100644 --- a/docs/use_cases/index.md +++ b/docs/use_cases/index.md @@ -19,7 +19,7 @@ This prevents silent failures or misconfigurations by enforcing structural and r --8<-- "examples/plugin_based_architecture/package.py" ``` -See also [Embedded Mode](../../modes/embedded.md) and [Contract Syntax](../../contracts/syntax.md) for YAML details. +See also [Embedded Mode](../modes/embedded.md) and [Contract Syntax](../contracts/syntax.md) for YAML details. --- @@ -37,7 +37,7 @@ Typical use cases: importspy extensions.py -s spymodel.yml -l INFO ``` -See [CLI Mode](../../modes/cli.md) for full usage. +See [CLI Mode](../modes/cli.md) for full usage. --- diff --git a/mkdocs.yml b/mkdocs.yml index 87e3902..6fab34e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -5,6 +5,8 @@ repo_name: importspy/importspy theme: name: material + custom_dir: docs/overrides + favicon: assets/favicon.ico logo: assets/importspy-logo.png palette: - scheme: default @@ -31,8 +33,8 @@ nav: - SpyModel Architecture: advanced/spymodel.md - Violation System: advanced/violations.md - Validation & Errors: errors/contract-violations.md + - Use Cases: use_cases/index.md - API Reference: api-reference.md - - Contributing: CONTRIBUTING.md plugins: - search diff --git a/src/importspy/utilities/__init__.py b/src/importspy/utilities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/importspy/utilities/module_util.py b/src/importspy/utilities/module_util.py index bf9d440..ea52a7c 100644 --- a/src/importspy/utilities/module_util.py +++ b/src/importspy/utilities/module_util.py @@ -1,25 +1,26 @@ """ -Module: Module Utilities +Module utilities for runtime introspection and structure extraction. -This module provides a comprehensive set of utility functions for dynamic module inspection, -loading, unloading, and metadata extraction. It is designed to support ImportSpy's runtime -validation processes by enabling detailed analysis of Python modules. +This module provides utility functions for analyzing Python modules dynamically, +primarily to support ImportSpy's runtime validation mechanisms. It enables inspection +of modules, their metadata, and internal structure at runtime. -Key Features: -------------- -- Inspect the calling stack and retrieve information about modules. -- Dynamically load and unload modules for runtime modifications. -- Extract metadata such as classes, functions, variables, and inheritance hierarchies from modules. - -Example Usage: --------------- -.. code-block:: python +Features: +- Inspect the call stack and determine caller modules. +- Dynamically load and unload Python modules. +- Extract version information via metadata or attributes. +- Retrieve global variables, top-level functions, and class definitions. +- Analyze methods, attributes (class-level and instance-level), and superclasses. +Example: + ```python from importspy.utilities.module_util import ModuleUtil + import inspect module_util = ModuleUtil() - module_info = module_util.get_info_module(inspect.stack()[0]) - print(f"Module Name: {module_info.__name__}") + info = module_util.get_info_module(inspect.stack()[0]) + print(info.__name__) + ``` """ import inspect @@ -43,23 +44,11 @@ class ModuleUtil: """ - Utility class for dynamic module inspection and metadata extraction. - - The `ModuleUtil` class provides methods to inspect, load, unload, and analyze Python - modules at runtime. These utilities are essential for enabling ImportSpy's dynamic - validation of runtime conditions. - - Methods: - -------- - - `inspect_module()`: Retrieve current and caller frames. - - `get_info_module()`: Extract module from a caller frame. - - `load_module()`: Dynamically reload a module. - - `unload_module()`: Remove module from memory. - - `extract_version()`: Retrieve version of a module. - - `extract_variables()`: Extract global variables. - - `extract_functions()`: Extract top-level functions. - - `extract_classes()`: Extract class definitions. - - `extract_superclasses()`: Collect all used base classes. + Provides methods to inspect and extract structural metadata from Python modules. + + This class enables runtime inspection of loaded modules for metadata such as + functions, classes, variables, inheritance hierarchies, and version information. + It is a core component used by ImportSpy to validate structural contracts. """ def inspect_module(self) -> tuple: @@ -67,9 +56,7 @@ def inspect_module(self) -> tuple: Retrieve the current and caller frames from the stack. Returns: - -------- - tuple - A tuple containing the current and caller frame. + tuple: A tuple with the current and the outermost caller frame. """ stack = inspect.stack() current_frame = stack[1] @@ -78,33 +65,25 @@ def inspect_module(self) -> tuple: def get_info_module(self, caller_frame: inspect.FrameInfo) -> ModuleType | None: """ - Retrieves the module object from a caller frame. + Resolve a module object from a given caller frame. - Parameters: - ----------- - caller_frame : inspect.FrameInfo - The frame to analyze. + Args: + caller_frame (inspect.FrameInfo): The caller frame to analyze. Returns: - -------- - ModuleType | None - The resolved module or None if not found. + ModuleType | None: The resolved module or None if not found. """ return inspect.getmodule(caller_frame.frame) def load_module(self, info_module: ModuleType) -> ModuleType | None: """ - Dynamically reload a module. + Reload a module dynamically from its file location. - Parameters: - ----------- - info_module : ModuleType - The module reference. + Args: + info_module (ModuleType): The module to reload. Returns: - -------- - ModuleType | None - The reloaded module. + ModuleType | None: The reloaded module or None if loading fails. """ spec = importlib.util.spec_from_file_location(info_module.__name__, info_module.__file__) if spec and spec.loader: @@ -116,12 +95,10 @@ def load_module(self, info_module: ModuleType) -> ModuleType | None: def unload_module(self, module: ModuleType): """ - Removes a module from memory. + Unload a module from sys.modules and globals. - Parameters: - ----------- - module : ModuleType - The module to unload. + Args: + module (ModuleType): The module to unload. """ module_name = module.__name__ if module_name in sys.modules: @@ -130,16 +107,13 @@ def unload_module(self, module: ModuleType): def extract_version(self, info_module: ModuleType) -> str | None: """ - Retrieves version metadata for the module. + Attempt to retrieve the version string from a module. - Parameters: - ----------- - info_module : ModuleType + Args: + info_module (ModuleType): The target module. Returns: - -------- - str | None - The version string if found. + str | None: Version string if found, otherwise None. """ if hasattr(info_module, '__version__'): return info_module.__version__ @@ -150,7 +124,13 @@ def extract_version(self, info_module: ModuleType) -> str | None: def extract_annotation(self, annotation) -> Optional[str]: """ - Converts annotations to string format for validation. + Convert a type annotation object into a string representation. + + Args: + annotation: The annotation object to convert. + + Returns: + Optional[str]: The extracted annotation string or None. """ if annotation == inspect._empty or not annotation: return None @@ -158,8 +138,17 @@ def extract_annotation(self, annotation) -> Optional[str]: return annotation.__name__ return str(annotation) - def extract_variables(self, info_module: ModuleType) -> dict: - variables_info:List[VariableInfo] = [] + def extract_variables(self, info_module: ModuleType) -> List[VariableInfo]: + """ + Extract top-level variable definitions from a module. + + Args: + info_module (ModuleType): The module to analyze. + + Returns: + List[VariableInfo]: List of variable metadata. + """ + variables_info: List[VariableInfo] = [] for name, value in inspect.getmembers(info_module): if not name.startswith('__') and not inspect.ismodule(value) and not inspect.isfunction(value) and not inspect.isclass(value): annotation = self.extract_annotation(type(value)) @@ -168,11 +157,13 @@ def extract_variables(self, info_module: ModuleType) -> dict: def extract_functions(self, info_module: ModuleType) -> List[FunctionInfo]: """ - Extracts function definitions from the module. + Extract all functions defined at the top level of the module. + + Args: + info_module (ModuleType): The target module. Returns: - -------- - List[FunctionInfo] + List[FunctionInfo]: Function metadata extracted from the module. """ functions_info: List[FunctionInfo] = [] for name, obj in inspect.getmembers(info_module, inspect.isfunction): @@ -182,7 +173,14 @@ def extract_functions(self, info_module: ModuleType) -> List[FunctionInfo]: def _extract_function(self, name: str, obj: FunctionType) -> FunctionInfo: """ - Builds metadata for a function. + Build structured metadata for a function. + + Args: + name (str): Function name. + obj (FunctionType): Function object. + + Returns: + FunctionInfo: Extracted function metadata. """ return FunctionInfo( name, @@ -192,7 +190,13 @@ def _extract_function(self, name: str, obj: FunctionType) -> FunctionInfo: def _extract_arguments(self, obj: FunctionType) -> List[ArgumentInfo]: """ - Retrieves argument names and annotations. + Extract arguments from a function's signature. + + Args: + obj (FunctionType): Function object. + + Returns: + List[ArgumentInfo]: List of function argument metadata. """ args = [] for name, param in inspect.signature(obj).parameters.items(): @@ -202,7 +206,13 @@ def _extract_arguments(self, obj: FunctionType) -> List[ArgumentInfo]: def extract_methods(self, cls_obj) -> List[FunctionInfo]: """ - Extracts all method definitions from a class. + Extract method definitions from a class object. + + Args: + cls_obj: The class to inspect. + + Returns: + List[FunctionInfo]: Extracted method metadata. """ methods: List[FunctionInfo] = [] for name, obj in inspect.getmembers(cls_obj, inspect.isfunction): @@ -212,7 +222,14 @@ def extract_methods(self, cls_obj) -> List[FunctionInfo]: def extract_attributes(self, cls_obj, info_module: ModuleType) -> List[AttributeInfo]: """ - Extracts class-level and instance-level attributes. + Extract both class-level and instance-level attributes. + + Args: + cls_obj: The class to analyze. + info_module (ModuleType): The module containing the class. + + Returns: + List[AttributeInfo]: List of extracted attributes. """ attributes: List[AttributeInfo] = [] annotations = getattr(cls_obj, '__annotations__', {}) @@ -243,11 +260,13 @@ def extract_attributes(self, cls_obj, info_module: ModuleType) -> List[Attribute def extract_classes(self, info_module: ModuleType) -> List[ClassInfo]: """ - Extracts class definitions from the module. + Extract all class definitions from a module. + + Args: + info_module (ModuleType): The module to inspect. Returns: - -------- - List[ClassInfo] + List[ClassInfo]: Metadata about the module’s classes. """ classes = [] for name, cls in inspect.getmembers(info_module, inspect.isclass): @@ -257,8 +276,16 @@ def extract_classes(self, info_module: ModuleType) -> List[ClassInfo]: classes.append(ClassInfo(name, attributes, methods, superclasses)) return classes - def extract_superclasses(self, cls) -> List[ClassInfo]: + """ + Extract base classes for a given class, recursively. + + Args: + cls: The class whose base classes are being extracted. + + Returns: + List[ClassInfo]: Metadata for each superclass. + """ superclasses = [] for base in cls.__bases__: if base.__name__ == "object": @@ -273,5 +300,3 @@ def extract_superclasses(self, cls) -> List[ClassInfo]: [] )) return superclasses - - diff --git a/src/importspy/utilities/python_util.py b/src/importspy/utilities/python_util.py index d8f2b9d..0e37d53 100644 --- a/src/importspy/utilities/python_util.py +++ b/src/importspy/utilities/python_util.py @@ -1,22 +1,20 @@ -""" -Python Runtime Utilities -======================== +"""Python Runtime Utilities -Provides helpers to inspect the active Python environment, -such as the interpreter implementation and version. +Provides utility methods to inspect the active Python runtime environment, +such as the version number and interpreter implementation. -Useful in ImportSpy to validate compatibility constraints across Python versions -and runtime variants (e.g., CPython, PyPy, IronPython). +These utilities are useful within ImportSpy to evaluate whether the current +runtime context satisfies declared compatibility constraints in import contracts. +This includes checks for specific Python versions and interpreter families +(CPython, PyPy, IronPython, etc.). Example: --------- -.. code-block:: python - - from importspy.utilities.python_util import PythonUtil - - util = PythonUtil() - print(util.extract_python_version()) - print(util.extract_python_implementation()) + >>> from importspy.utilities.python_util import PythonUtil + >>> util = PythonUtil() + >>> util.extract_python_version() + '3.12.0' + >>> util.extract_python_implementation() + 'CPython' """ import logging @@ -25,43 +23,42 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) -class PythonUtil: - """ - Utility class for querying Python runtime details. - Methods - ------- - extract_python_version() -> str - Returns the current Python version (e.g., "3.11.2"). +class PythonUtil: + """Utility class for inspecting Python runtime characteristics. - extract_python_implementation() -> str - Returns the Python interpreter name (e.g., "CPython", "PyPy"). + Used internally by ImportSpy to validate runtime-specific conditions declared + in `.yml` import contracts. This includes checking Python version and interpreter + type during structural introspection and contract validation. """ def extract_python_version(self) -> str: - """ - Return the active Python version. + """Return the currently active Python version as a string. - Returns - ------- - str - Python version string (e.g., '3.11.2'). + This method queries the runtime using `platform.python_version()` and is + typically used to match version constraints defined in an import contract. + + Returns: + str: The Python version string (e.g., "3.11.4"). + + Example: + >>> PythonUtil().extract_python_version() + '3.11.4' """ - python_version = platform.python_version() - return python_version + return platform.python_version() def extract_python_implementation(self) -> str: - """ - Return the Python implementation type. + """Return the implementation name of the running Python interpreter. - Returns - ------- - str - Python interpreter name (e.g., 'CPython', 'PyPy'). + Common values include "CPython", "PyPy", or "IronPython". This is + essential in contexts where the implementation affects runtime behavior + or compatibility with native extensions. - Example - ------- - >>> PythonUtil().extract_python_implementation() - 'CPython' + Returns: + str: The interpreter implementation (e.g., "CPython"). + + Example: + >>> PythonUtil().extract_python_implementation() + 'CPython' """ return platform.python_implementation() diff --git a/src/importspy/utilities/runtime_util.py b/src/importspy/utilities/runtime_util.py index f19ebfe..60ec06e 100644 --- a/src/importspy/utilities/runtime_util.py +++ b/src/importspy/utilities/runtime_util.py @@ -1,20 +1,16 @@ -""" -Runtime Environment Utilities -============================= - -Provides a lightweight interface to query the system's hardware architecture. - -This module supports ImportSpy in enforcing architecture-specific constraints -declared in import contracts. +"""Runtime Environment Utilities -Example -------- -.. code-block:: python +Provides a lightweight utility for querying the system's hardware architecture. - from importspy.utilities.runtime_util import RuntimeUtil +This module is used by ImportSpy to enforce architecture-specific constraints +defined in import contracts (e.g., allowing a plugin only on x86_64 or arm64). +It ensures that module imports are aligned with the intended deployment environment. - runtime = RuntimeUtil() - print(runtime.extract_arch()) +Example: + >>> from importspy.utilities.runtime_util import RuntimeUtil + >>> runtime = RuntimeUtil() + >>> runtime.extract_arch() + 'x86_64' """ import logging @@ -23,30 +19,28 @@ logger = logging.getLogger(__name__) logger.addHandler(logging.NullHandler()) -class RuntimeUtil: - """ - Utility class to retrieve system architecture details. - Methods - ------- - extract_arch() -> str - Returns the machine’s hardware architecture. +class RuntimeUtil: + """Utility class to inspect system architecture. - Example - ------- - >>> RuntimeUtil().extract_arch() - 'x86_64' + This class provides methods to retrieve runtime hardware architecture details, + which are essential when validating platform-specific import constraints + in ImportSpy's embedded or CLI modes. """ def extract_arch(self) -> str: - """ - Return the system architecture (e.g., 'x86_64', 'arm64'). - - Uses the `platform.machine()` method to query the current hardware. - - Returns - ------- - str - Architecture name (e.g., 'x86_64', 'arm64'). + """Return the name of the machine's hardware architecture. + + Uses `platform.machine()` to retrieve the architecture string, which may vary + depending on the underlying system (e.g., "x86_64", "arm64", "aarch64"). + This is typically used during contract validation to ensure that the importing + environment matches expected deployment conditions. + + Returns: + str: The system's hardware architecture. + + Example: + >>> RuntimeUtil().extract_arch() + 'arm64' """ return platform.machine() diff --git a/src/importspy/utilities/system_util.py b/src/importspy/utilities/system_util.py index 401d428..5fb2709 100644 --- a/src/importspy/utilities/system_util.py +++ b/src/importspy/utilities/system_util.py @@ -1,26 +1,23 @@ -""" -System Utilities for ImportSpy -============================== +"""System Utilities for ImportSpy -Provides tools to interact with the host system and environment variables. +Provides tools to inspect the host operating system and environment variables. -This utility module helps ImportSpy detect and normalize runtime conditions, such as -the operating system or environment setup, ensuring compatibility checks work reliably. +This module supports ImportSpy by normalizing system-level information that may +affect import contract validation. It helps ensure that environmental conditions +are consistent and inspectable across different operating systems and deployment contexts. Features: ---------- -- Identifies the current operating system in a standardized lowercase format. -- Retrieves environment variables as a key-value dictionary. + - Detects the current operating system in a normalized, lowercase format. + - Retrieves all environment variables as a list of structured objects. Example: --------- -.. code-block:: python - - from importspy.utilities.system_util import SystemUtil - - util = SystemUtil() - os_name = util.extract_os() - env = util.extract_envs() + >>> from importspy.utilities.system_util import SystemUtil + >>> util = SystemUtil() + >>> util.extract_os() + 'linux' + >>> envs = util.extract_envs() + >>> envs[0] + VariableInfo(name='PATH', annotation=None, value='/usr/bin') """ import os @@ -34,55 +31,49 @@ VariableInfo = namedtuple('VariableInfo', ["name", "annotation", "value"]) -class SystemUtil: - """ - System-level utility class for environment inspection. - Offers support for OS detection and retrieval of environment variables. +class SystemUtil: + """Utility class for inspecting system-level properties. - Methods - ------- - extract_os() -> str - Returns the lowercase name of the operating system (e.g., 'windows', 'linux'). + Used by ImportSpy to collect information about the current operating system + and active environment variables. These details are typically validated + against constraints defined in `.yml` import contracts. - extract_envs() -> dict - Returns a dictionary of all active environment variables. + Methods: + extract_os(): Return the normalized name of the current operating system. + extract_envs(): Return all active environment variables as structured entries. """ def extract_os(self) -> str: - """ - Return the operating system name in lowercase. + """Return the name of the operating system in lowercase format. - Uses `platform.system()` for OS detection. + This method uses `platform.system()` and normalizes the result + to lowercase. It simplifies comparisons with import contract conditions + that expect a canonical form such as "linux", "darwin", or "windows". - Returns - ------- - str - 'windows', 'linux', or 'darwin', depending on the system. + Returns: + str: The normalized operating system name (e.g., "linux", "windows"). - Example - ------- - >>> SystemUtil().extract_os() - 'linux' + Example: + >>> SystemUtil().extract_os() + 'darwin' """ return platform.system().lower() def extract_envs(self) -> List[VariableInfo]: - """ - Return a list of environment variables available in the current process. + """Return all environment variables as a list of structured objects. - Uses `os.environ.items()` to collect all key-value pairs. + Collects all key-value pairs from `os.environ` and wraps them in + `VariableInfo` namedtuples. The `annotation` field is reserved for + optional type annotation metadata (currently set to `None`). - Returns - ------- - List[VariableInfo] + Returns: + List[VariableInfo]: A list of environment variables available + in the current process environment. - A list of VariableInfo instances, each representing an environment variable. - - Example - ------- - >>> SystemUtil().extract_envs() - [VariableInfo(name='PATH', value='/usr/bin'), VariableInfo(name='HOME', value='/home/user'), ...] + Example: + >>> envs = SystemUtil().extract_envs() + >>> envs[0] + VariableInfo(name='PATH', annotation=None, value='/usr/bin') """ return [VariableInfo(name, None, value) for name, value in os.environ.items()] - From b0ad99b10480c1796526a1469778a1d4fab82876 Mon Sep 17 00:00:00 2001 From: Luca Atella Date: Thu, 7 Aug 2025 20:06:26 +0200 Subject: [PATCH 40/40] chore(readme): fix media reference --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index e8ce70f..c394218 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Build Status](https://img.shields.io/github/actions/workflow/status/atellaluca/ImportSpy/python-package.yml?branch=main)](https://github.com/atellaluca/ImportSpy/actions/workflows/python-package.yml) [![Docs](https://img.shields.io/readthedocs/importspy)](https://importspy.readthedocs.io/) -![ImportSpy banner](./assets/importspy-banner.png) +![ImportSpy banner](https://github.com/atellaluca/ImportSpy/docs/assets/importspy-banner.png) **Runtime contract validation for Python imports.** _Enforce structure. Block invalid usage. Stay safe at runtime._ @@ -48,7 +48,7 @@ pip install importspy ## 📐 Architecture -![SpyModel UML](./assets/importspy-spy-model-architecture.png) +![SpyModel UML](https://github.com/atellaluca/ImportSpy/docs/assets/importspy-spy-model-architecture.png) ImportSpy is powered by a layered introspection model (`SpyModel`), which captures: @@ -93,7 +93,7 @@ caller = Spy().importspy(filepath="spymodel.yml") caller.Plugin().run() ``` -![Embedded mode](./assets/importspy-embedded-mode.png) +![Embedded mode](https://github.com/atellaluca/ImportSpy/docs/assets/importspy-embedded-mode.png) --- @@ -103,7 +103,7 @@ caller.Plugin().run() importspy -s spymodel.yml -l DEBUG path/to/module.py ``` -![CLI mode](./assets/importspy-works.png) +![CLI mode](https://github.com/atellaluca/ImportSpy/docs/assets/importspy-works.png) --- @@ -175,4 +175,4 @@ to how, when, and where modules are imported. ## 📜 License MIT © 2024 – Luca Atella -![ImportSpy logo](./assets/importspy-logo.png) +![ImportSpy logo](https://github.com/atellaluca/ImportSpy/docs/assets/importspy-logo.png)