diff --git a/.claude/skills/review-code/SKILL.md b/.claude/skills/review-code/SKILL.md new file mode 100644 index 0000000..ec32655 --- /dev/null +++ b/.claude/skills/review-code/SKILL.md @@ -0,0 +1,49 @@ +--- +name: review-code +description: Reviews code changes for bugs, style issues, and correctness. Use when reviewing code, PRs, or asking for a code review. +allowed-tools: Read, Grep, Glob, Bash +--- + +Review the code changes specified by `$ARGUMENTS` (a file path, directory, or git ref range). If no arguments are provided, review all uncommitted changes using `git diff` and `git diff --cached`. + +## Review Checklist + +### Correctness +- Logic errors, off-by-one mistakes, missing edge cases +- Proper handling of `None`/`undef` values throughout the AST pipeline +- Parse tree visitor methods in `builder.py` must match grammar rule names in `grammar.py` +- AST node dataclass fields must match what the builder produces + +### Parser & Grammar +- New grammar rules must be reachable from `openscad_language` or `openscad_language_with_comments` +- Arpeggio ordering matters: alternatives are tried in order, put more specific rules first +- Ensure `comment` and `whitespace_only` skip rules aren't accidentally changed +- Token rules using `Not()` must not create ambiguity with other tokens + +### AST Layer +- New node types must inherit from `ASTNode` and use `@dataclass` +- Nodes must be serializable (check `serialization.py` compatibility) +- Source position tracking via `SourceMap` must be preserved through transformations +- Scope handling: verify parent/child scope relationships are set correctly + +### Python Style +- Type annotations on public API functions +- Dataclass fields should have sensible defaults where appropriate +- Avoid mutable default arguments (use `field(default_factory=...)`) +- Follow existing patterns in the codebase rather than introducing new conventions + +### Testing +- New grammar rules need corresponding test cases in `tests/` +- Test both valid parses and expected parse failures +- AST builder changes need tests verifying node structure +- Use the existing `parser` and `parser_reduced` fixtures from `conftest.py` + +## Output Format + +Organize findings by severity: + +1. **Bugs** - Will cause incorrect behavior or crashes +2. **Issues** - Could cause problems in some cases +3. **Suggestions** - Improvements to readability, style, or maintainability + +For each finding, include the file path, line number, and a specific fix. diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..861e935 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,45 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install uv + run: pip install uv + + - name: Build package + run: uv build + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + needs: build + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + + steps: + - name: Download build artifacts + uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/docs/SCOPING_RULES.md b/docs/SCOPING_RULES.md new file mode 100644 index 0000000..02d894e --- /dev/null +++ b/docs/SCOPING_RULES.md @@ -0,0 +1,778 @@ +# OpenSCAD Scoping Rules + +This document provides a comprehensive reference for OpenSCAD's variable scoping rules. It covers both lexical scoping for regular variables and dynamic scoping for `$`-prefixed special variables. This serves as the specification for implementing scope-aware AST traversal. + +## Table of Contents + +1. [Introduction](#1-introduction) +2. [Lexical Scoping (Regular Variables)](#2-lexical-scoping-regular-variables) +3. [Dynamic Scoping ($-prefixed Variables)](#3-dynamic-scoping--prefixed-variables) +4. [Namespaces and Resolution Rules](#4-namespaces-and-resolution-rules) +5. [Use vs Include Behavior](#5-use-vs-include-behavior) +6. [Edge Cases and Special Behaviors](#6-edge-cases-and-special-behaviors) +7. [Examples](#7-examples) +8. [AST Node Reference Table](#8-ast-node-reference-table) + +--- + +## 1. Introduction + +OpenSCAD uses a **dual scoping model**: + +- **Lexical scoping** for regular variables (e.g., `x`, `width`, `my_value`) +- **Dynamic scoping** for special variables prefixed with `$` (e.g., `$fn`, `$fa`, `$fs`) + +This combination is unusual among programming languages and requires careful understanding to use effectively. + +### Key Differences from C-like Languages + +- **Limited block scoping**: Bare braces `{ }` do NOT create new scopes, but `if/else` branches DO +- **Single-assignment semantics**: Variables cannot be reassigned within a scope (but can be shadowed) +- **Hoisted evaluation**: All assignments in a scope are evaluated before any module instantiations +- **Three separate namespaces**: Functions, modules, and variables can share the same name +- **Lexical closure**: Modules and functions inherit the lexical scope where they were *declared*, not where they are *called* + +--- + +## 2. Lexical Scoping (Regular Variables) + +### 2.1 Single-Assignment Semantics + +Within a single scope level, a variable name can only be assigned once. However, inner scopes can **shadow** variables from outer scopes by using the same name. + +```openscad +x = 10; +// x = 20; // ERROR: Cannot reassign x in the same scope + +module foo() { + x = 20; // OK: This shadows the outer x + echo(x); // Outputs: 20 +} +``` + +### 2.2 What Does NOT Create a Scope + +Unlike C/C++ and many other languages, the following constructs do **NOT** create new variable scopes: + +#### Bare Braces `{ }` + +Braces are merely statement grouping, not scope boundaries: + +```openscad +{ + y = 42; +} +echo(y); // Works! Outputs: 42 - y "leaks" out of the braces +``` + +#### Module Calls + +When you call a module, the module executes in its own scope, but the **call site** does not create a new scope: + +```openscad +cube(10); // No new scope created at this line +x = 5; // x is in the same scope as the cube() call +``` + +### 2.3 Scope-Creating Constructs + +The following constructs **DO** create new variable scopes: + +| Construct | AST Node | Scope Contains | +|-----------|----------|----------------| +| Module declaration | `ModuleDeclaration` | Parameters + body (including nested function/module declarations) | +| Function declaration | `FunctionDeclaration` | Parameters + expression | +| Expression-level let | `LetOp` | Assignments + trailing expression | +| Statement-level let | `ModularLet` | Assignments + children | +| If statement | `ModularIf` | True branch only | +| If-else statement | `ModularIfElse` | Each branch creates separate scope | +| For loop (statement) | `ModularFor` | Loop variables + body | +| C-style for (statement) | `ModularCFor` | Loop variables + body | +| Intersection for | `ModularIntersectionFor` | Loop variables + body | +| C-style intersection for | `ModularIntersectionCFor` | Loop variables + body | +| Module instantiation children | `ModularCall` (children) | Children of a module call | +| For loop (list comprehensions) | `ListCompFor` | Loop variables + body | +| C-style for (list comprehensions) | `ListCompCFor` | Loop variables + body | +| Let (list comprehensions) | `ListCompLet` | Assignments + body | +| Anonymous function | `FunctionLiteral` | Parameters + body | + +**Note:** `ListCompIf` and `ListCompIfElse` do **NOT** create new scopes - they are expression-level conditional filters within list comprehensions, not modular scope boundaries. + +#### Module Declaration + +When a module is instantiated, it can see: + +1. **Parameters** defined in the module declaration +2. **Lexical scope from where it was declared** (not where it's called) +3. **$variables** inherited dynamically from the caller + +**Nested Declarations**: Modules and functions can be declared inside a module body. These are scoped to the containing module and not visible outside: + +```openscad +module outer() { + function helper(x) = x * 2; // Only visible inside outer() + module inner() { cube(1); } // Only visible inside outer() + + inner(); // OK + echo(helper(5)); // OK +} + +// helper() and inner() are NOT visible here +inner(); // ERROR: inner is not defined +``` + +```openscad +decl_scope_var = 100; + +module foo() { + echo(decl_scope_var); // Sees 100 from declaration scope +} + +module bar() { + decl_scope_var = 999; // Local to bar + foo(); // foo still sees 100, not 999! +} + +bar(); // Outputs: 100 +``` + +The key distinction: regular variables come from **declaration scope**, `$variables` come from **call scope**: + +```openscad +x = 10; +$y = 10; + +module inner() { + echo("x =", x); // From declaration scope + echo("$y =", $y); // From call scope (dynamic) +} + +module outer() { + x = 20; // Local x (shadows, but inner doesn't see it) + $y = 20; // $y inherited by children + inner(); +} + +outer(); +// Outputs: +// x = 10 (lexical - from where inner was declared) +// $y = 20 (dynamic - from where inner was called) +``` + +#### Function Declaration + +Functions see their parameters plus variables from the complete scope **where they were declared**: + +```openscad +scale_factor = 2; + +function scaled_area(w, h) = w * h * scale_factor * aspect; + +aspect = 1.5; + +// w and h are parameters +// scale_factor and aspect are from declaration scope + +echo(scaled_area(3, 4)); // Outputs: 36 + +module test() { + scale_factor = 100; // Local to test + echo(scaled_area(3, 4)); // Still outputs 36! Uses scale_factor=2 +} +``` + +#### Expression-Level Let (`LetOp`) + +```openscad +x = let(foo=3, bar=4) foo * bar; +// foo and bar are ONLY in scope for the expression "foo * bar" +// x gets the value 12 +echo(foo); // ERROR: foo is not defined here +``` + +#### Statement-Level Let (`ModularLet`) + +```openscad +let(foo=3, bar=4) + square([foo, bar]); +// foo and bar are in scope for the children of let() +``` + +#### If/Else Statements (`ModularIf`, `ModularIfElse`) + +Variables assigned inside if/else branches are only visible within that branch: + +```openscad +if (condition) { + x = 10; + cube(x); // OK: x is in scope +} +echo(x); // ERROR: x is not defined here +``` + +Each branch creates its own scope: + +```openscad +if (flag) { + val = 100; +} else { + val = 200; +} +echo(val); // ERROR: val is not defined here +``` + +#### For Loops + +```openscad +for (i = [0:10]) { + // i is in scope here + cube(i); +} +// i is NOT in scope here +``` + +### 2.4 Shadowing Rules + +Inner scopes can shadow variables from outer scopes: + +```openscad +x = 10; + +module test() { + x = 20; // Shadows outer x + echo(x); // Outputs: 20 +} + +test(); +echo(x); // Outputs: 10 (outer x unchanged) +``` + +Shadowing also works with parameters: + +```openscad +x = 10; + +module test(x) { // Parameter x shadows global x + echo(x); +} + +test(99); // Outputs: 99 +``` + +### 2.5 Assignment Scoping Rule + +A variable declared by an assignment statement does **not** come into scope until **after** the assignment statement. The right-hand side expression cannot reference the variable being defined: + +```openscad +x = x + 1; // ERROR: x is not defined in the RHS expression +y = 10; +z = y + 1; // OK: y is already defined +``` + +**Exception - Function Literals**: The body of a function literal inherits the complete scope of where is is declared. This enables recursion: + +```openscad +fn = function(a) a<2? 1 : a * fn(a-1) + b; // fn and b are both in scope inside the function literal +b = 3; +``` + +### 2.6 Evaluation Order Within a Scope Level (Hoisting) + +**Critical rule**: Within any **modular sub-block**, all declarations (assignments, function declarations, module declarations) are collected **BEFORE** any module instantiations are evaluated. + +Modular sub-blocks include: +- Top-level file scope +- Module declaration bodies +- Each branch of `ModularIf` / `ModularIfElse` +- Body of `ModularFor` / `ModularCFor` / `ModularIntersectionFor` / `ModularIntersectionCFor` +- Children of `ModularCall` (module instantiation children) +- Children of `ModularLet` / `ModularEcho` / `ModularAssert` + +This means assignments are effectively "hoisted" - a variable assigned later in the block is visible to module calls that appear earlier in the source code: + +```openscad +module test() { + cube(x); // Uses x = 20, not undefined! + x = 10; + sphere(x); // Uses x = 20 + x = 20; // This is the final value of x +} +``` + +**Evaluation sequence:** +1. `x = 10` is evaluated +2. `x = 20` is evaluated (x is now 20) +3. `cube(x)` is instantiated with x = 20 +4. `sphere(x)` is instantiated with x = 20 + +**Key distinction**: Hoisting applies to module instantiations seeing variables, but the RHS of an assignment still cannot see the variable being defined (see Section 2.5). + +### 2.7 Multiple Assignments at Same Scope Level + +When the same variable is assigned multiple times at the same scope level, the **last assignment wins**: + +```openscad +x = 1; +x = 2; +x = 3; +echo(x); // Outputs: 3 +``` + +This combines with the hoisting rule to create potentially surprising behavior (see Section 2.6). + +### 2.8 Default Parameter Evaluation + +Default parameter values are evaluated in the scope of the place where the function, module, or function literal are declared: + +```openscad +y = 100; + +module test(x = y) { + y = 50; // This does NOT affect the default + echo(x); +} + +test(); // Outputs: 100 +if (true) { + y = 25; + test(); // Still outputs 100. +} +``` + +--- + +## 3. Dynamic Scoping ($-prefixed Variables) + +Variables prefixed with `$` use **dynamic scoping**, which means their values are inherited through the call chain at runtime, not determined by lexical (textual) location. + +### 3.1 How $variables Differ from Regular Variables + +| Aspect | Regular Variables | $variables | +|--------|-------------------|------------| +| Scoping | Lexical (textual) | Dynamic (runtime call chain) | +| Inherited from | Scope where function/module was **declared** | Scope where function/module was **called** | +| Use case | Local computation | Configuration, context passing | + +### 3.2 Inheritance Through Module Calls + +When a module is called, it inherits all `$variables` from its caller: + +```openscad +$my_setting = 10; + +module parent() { + $my_setting = 20; + child(); // child sees $my_setting = 20 +} + +module child() { + echo($my_setting); +} + +parent(); // Outputs: 20 +child(); // Outputs: 10 (called from top level) +``` + +### 3.3 Children Inherit Parent's $variable Scope + +When a module is called with children, the children see the `$variables` as modified inside the parent module. + +**Syntax note:** Braces are only required for multiple children. A single child can be passed without braces: + +```openscad +parent() child(); // Single child - no braces needed +parent() { child1(); child2(); } // Multiple children - braces required +``` + +Example of $variable inheritance: + +```openscad +$size = 5; + +module enlarge() { + $size = $size * 2; + children(); +} + +enlarge() { + cube($size); // Uses $size = 10, not 5! +} +``` + +This is **dynamic scoping** - the child sees the `$variable` values from where it's *called*, not where it's *defined*. + +### 3.4 Common Built-in $variables + +| Variable | Purpose | Default | +|----------|---------|---------| +| `$fn` | Number of fragments for circles/spheres | 0 (auto) | +| `$fa` | Minimum angle in degrees for fragments | 12 | +| `$fs` | Minimum size/length for fragments | 2 | +| `$t` | Animation time (0.0 to 1.0) | 0.0 | +| `$children` | Number of child modules | 0 | +| `$preview` | True if in preview mode | - | +| `$parent_modules` | Stack of parent module names | - | + +### 3.5 User-Defined $variables + +You can define your own `$variables` for passing configuration down the call chain: + +```openscad +$wall_thickness = 2; +$tolerance = 0.2; + +module box() { + // Uses $wall_thickness and $tolerance from caller + difference() { + cube([10, 10, 10]); + translate([$wall_thickness, $wall_thickness, $wall_thickness]) + cube([10 - 2*$wall_thickness, + 10 - 2*$wall_thickness, + 10 - $wall_thickness + $tolerance]); + } +} +``` + +### 3.6 Passing $variables as Module Arguments + +You can pass `$variables` as arguments when instantiating modules. These `$variables` are then available to the children of that module: + +```openscad +module foo() { + children(); +} + +foo($fee=7) { + square($fee); // Uses $fee = 7 +} +``` + +This allows you to pass `$variables` directly at the call site, making them available to the module's children without needing to assign them in the module body: + +```openscad +module container() { + children(); +} + +container($width=10, $height=5) { + cube([$width, $height, 2]); // Uses $width=10, $height=5 +} +``` + +A `$variable` passed as an argument will **shadow** any previous value of that `$variable` from the caller's scope: + +```openscad +$size = 5; + +module wrapper() { + children(); +} + +wrapper($size=20) { + cube($size); // Uses $size = 20, not 5 (argument shadows caller's value) +} +``` + +### 3.7 Recent Changes (Needs Research) + +> **Note:** There have been recent controversial changes to `$variable` scoping behavior at the top-level context. These changes were discussed on the OpenSCAD mailing list. The exact details need to be researched and documented here. + +--- + +## 4. Namespaces and Resolution Rules + +### 4.1 Three Independent Namespaces + +OpenSCAD maintains **three separate namespaces**: + +1. **Functions namespace** - for function declarations +2. **Modules namespace** - for module declarations +3. **Variables namespace** - for variable assignments + +The same identifier can exist in all three namespaces simultaneously: + +```openscad +foobar = 10; // Variable named foobar + +function foobar() = 20; // Function named foobar + +module foobar() { // Module named foobar + cube(30); +} + +echo(foobar); // Outputs: 10 (the variable) +echo(foobar()); // Outputs: 20 (the function) +foobar(); // Instantiates the module (cube) +``` + +### 4.2 Function Call Resolution in Expression Context + +When a call like `qux()` appears in an expression context (e.g., right side of assignment): + +1. **First**, check for a variable named `qux` in the local scope that might contain a function literal +2. **If not found**, look for a function named `qux` in the function namespace + +```openscad +function qux() = 10; + +x = qux(); // Calls the function, x = 10 + +qux = function() 20; // Variable holding function literal +y = qux(); // Calls the variable's function, y = 20 +``` + +### 4.3 Function vs Module Determination + +Whether a name resolves to a function or module depends on **context**: + +| Context | Resolution | +|---------|------------| +| Expression (e.g., assignment RHS) | Function | +| Statement/Instantiation | Module | + +```openscad +function cube(x) = x * x * x; // Function named cube +// module cube is built-in // Module named cube + +volume = cube(3); // Calls FUNCTION, volume = 27 +cube(3); // Instantiates MODULE (3D cube) +``` + +### 4.4 Variable Lookup Algorithm + +For regular variables, lookup proceeds from innermost to outermost scope: + +1. Check current scope (function body, module body, let bindings, etc.) +2. Check enclosing scopes, moving outward +3. Check global/file scope +4. If not found, a WARNING is issued that the variable is undefined + +### 4.5 Handling Undefined Variables + +Referencing an undefined variable throws a WARNING that the variable is undefined: + +```openscad +echo(nonexistent); // WARNING: Variable "nonexistent" is not defined +x = nonexistent + 1; // WARNING: Variable "nonexistent" is not defined +echo(x); // Outputs undef +``` + +--- + +## 5. Use vs Include Behavior + +### 5.1 `use ` + +The `use` statement imports **functions and modules** from another file, but **NOT variables**: + +```openscad +// library.scad +lib_var = 100; +function lib_func() = 42; +module lib_mod() { cube(10); } + +// main.scad +use + +echo(lib_var); // ERROR: lib_var not accessible +echo(lib_func()); // OK: Outputs 42 +lib_mod(); // OK: Creates cube +``` + +### 5.2 `include ` + +The `include` statement effectively **inserts the file contents** at that point, making all functions, modules, AND variables available: + +```openscad +// library.scad +lib_var = 100; +function lib_func() = 42; +module lib_mod() { cube(10); } + +// main.scad +include + +echo(lib_var); // OK: Outputs 100 +echo(lib_func()); // OK: Outputs 42 +lib_mod(); // OK: Creates cube +``` + +### 5.3 Summary + +| Statement | Functions | Modules | Variables | +|-----------|-----------|---------|-----------| +| `use ` | Imported | Imported | NOT imported | +| `include ` | Imported | Imported | Imported | + +--- + +## 6. Edge Cases and Special Behaviors + +### 6.1 Braces Don't Create Scopes + +Variables assigned inside braces are visible outside: + +```openscad +{ + inner = 42; +} +echo(inner); // Outputs: 42 +``` + +### 6.2 Variables in If/Else Branches + +Variables assigned in if/else branches are scoped to that branch only: + +```openscad +if (true) { + branch_var = 20; + echo(branch_var); // Outputs: 20 +} +echo(branch_var); // ERROR: branch_var not defined here +``` + +This is different from bare braces, which do NOT create a scope. + +### 6.3 Recursive Function/Module References + +Functions and modules can reference themselves: + +```openscad +function factorial(n) = n <= 1 ? 1 : n * factorial(n - 1); +echo(factorial(5)); // Outputs: 120 +``` + +### 6.4 Forward References Within Same Scope Level + +Due to hoisting, forward references work: + +```openscad +echo(later_var); // Outputs: 10 (due to hoisting) +later_var = 10; +``` + +### 6.5 Default Parameter Gotchas + +Default parameters **cannot** reference earlier parameters. They are evaluated in the caller's scope: + +```openscad +w = 100; +module box(w, h, d=w) { // d=w refers to global w, not parameter w + cube([w, h, d]); +} +box(10, 5); // Creates 10x5x100 cube (d uses global w=100, not parameter w=10) +``` + +Default parameter values are always evaluated in the caller's scope, not the function/module's parameter scope. + +--- + +## 7. Examples + +### 7.1 Shadowing Example + +```openscad +x = "global"; + +module outer() { + x = "outer"; + echo("In outer:", x); // Outputs: outer + + module inner() { + x = "inner"; + echo("In inner:", x); // Outputs: inner + } + + inner(); + echo("Back in outer:", x); // Outputs: outer +} + +outer(); +echo("At global:", x); // Outputs: global +``` + +### 7.2 $variable Inheritance Example + +```openscad +$color = "red"; + +module paint() { + $color = "blue"; + children(); +} + +module show_color() { + echo($color); +} + +show_color(); // Outputs: red + +paint() { + show_color(); // Outputs: blue (inherited from paint) +} +``` + +### 7.3 Evaluation Order Example + +```openscad +module demo() { + echo("a =", a); // Outputs: a = 3 (final value) + a = 1; + echo("b =", b); // Outputs: b = 2 (final value) + a = 2; + b = 2; + a = 3; +} +demo(); +``` + +### 7.4 Namespace Example + +```openscad +// All three coexist +thing = 10; +function thing() = 20; +module thing() { sphere(30); } + +x = thing; // x = 10 (variable) +y = thing(); // y = 20 (function) +thing(); // Creates sphere (module) +``` + +--- + +## 8. AST Node Reference Table + +Quick reference mapping AST nodes to their scope-related fields: + +| AST Node | Bindings Field | Scope Body Field | Notes | +|----------|---------------|------------------|-------| +| `ModuleDeclaration` | `parameters` | `children` | Parameters + nested functions/modules | +| `FunctionDeclaration` | `parameters` | `expr` | Parameters are `ParameterDeclaration` | +| `LetOp` | `assignments` | `body` | Expression-level let | +| `ModularLet` | `assignments` | `children` | Statement-level let | +| `ModularIf` | - | `true_branch` | Branch creates scope | +| `ModularIfElse` | - | `true_branch`, `false_branch` | Each branch creates separate scope | +| `ModularFor` | `assignments` | `body` | Loop variables in `assignments` | +| `ModularCFor` | `initial`, `increment` | `body` | C-style for loop | +| `ModularIntersectionFor` | `assignments` | `body` | Intersection for loop | +| `ModularIntersectionCFor` | `initial`, `increment` | `body` | C-style intersection for | +| `ModularCall` | - | `children` | Module instantiation children create scope | +| `ListCompFor` | `assignments` | `body` | List comprehension for | +| `ListCompCFor` | `initial`, `increment` | `body` | List comprehension c-for | +| `ListCompLet` | `assignments` | `body` | List comprehension let | +| `FunctionLiteral` | `arguments` | `body` | Anonymous function | +| `Assignment` | `name` | - | Top-level variable binding | +| `ParameterDeclaration` | `name`, `default` | - | Used in function/module params | + +**Note:** `ListCompIf` and `ListCompIfElse` do NOT create scopes - they are expression-level conditional filters. + +### Field Details + +- **`parameters`**: List of `ParameterDeclaration` nodes +- **`assignments`**: List of `Assignment` nodes +- **`children`**: List of `ModuleInstantiation` nodes +- **`body`**: Single expression or instantiation node +- **`initial`/`increment`**: Lists of `Assignment` nodes (C-style loops) +- **`name`**: `Identifier` node containing the variable name +- **`default`**: Optional `Expression` for default value diff --git a/pyproject.toml b/pyproject.toml index b5068d1..d3f48da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "uv_build" [project] name = "openscad_parser" -version = "2.0.1" +version = "2.1.0" description = "A PEG parser to read OpenSCAD language source code, with optional AST tree generation." readme = "README.rst" authors = [ diff --git a/src/openscad_parser/ast/__init__.py b/src/openscad_parser/ast/__init__.py index a935ae2..6397876 100644 --- a/src/openscad_parser/ast/__init__.py +++ b/src/openscad_parser/ast/__init__.py @@ -84,6 +84,9 @@ # Import ASTBuilderVisitor and Position from .builder import ASTBuilderVisitor, Position +# Import scope classes +from .scope import Scope, build_scopes + # Import serialization functions from .serialization import ( ast_to_dict, diff --git a/src/openscad_parser/ast/builder.py b/src/openscad_parser/ast/builder.py index 1beedf7..e525fc2 100644 --- a/src/openscad_parser/ast/builder.py +++ b/src/openscad_parser/ast/builder.py @@ -472,15 +472,18 @@ def visit_module_definition(self, node, children): statement = [] # pragma: no cover if not isinstance(statement, list): statement = [statement] - # Flatten nested statement lists and keep only ModuleInstantiation nodes + # Flatten nested statement lists. Include all statements (assignments, + # function/module declarations, module instantiations) so scope build + # can hoist declarations and attach scopes to every node. + # Filter out None values that may result from visit_statement() returning + # None for statement nodes with no children. flattened = [] stack = list(statement) while stack: item = stack.pop(0) if isinstance(item, list): stack = item + stack - continue - if isinstance(item, ModuleInstantiation): + elif item is not None: flattened.append(item) return ModuleDeclaration(name=name, parameters=parameters, children=flattened, position=self._get_node_position(node)) diff --git a/src/openscad_parser/ast/nodes.py b/src/openscad_parser/ast/nodes.py index 3bc7865..d7a012a 100644 --- a/src/openscad_parser/ast/nodes.py +++ b/src/openscad_parser/ast/nodes.py @@ -1,9 +1,10 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import TYPE_CHECKING if TYPE_CHECKING: from .builder import Position + from .scope import Scope # --- AST nodes classes. --- @@ -11,19 +12,27 @@ @dataclass class ASTNode(object): """Base class for all AST nodes. - + All AST nodes in the OpenSCAD parser inherit from this class. It provides a common interface for source position tracking and string representation. - + Attributes: position: The source position of this node in the original OpenSCAD code. + scope: The lexical scope at this node's location, populated by build_scope(). + This attribute is set dynamically after AST construction and may be None + if build_scope() has not been called. Access via node.scope. """ position: "Position" + scope: "Scope | None" = field(default=None, kw_only=True) def __str__(self) -> str: """Return a string representation of the AST node.""" raise NotImplementedError + def build_scope(self, parent_scope: "Scope") -> None: + """Assign parent_scope to this node. Leaf nodes use this default.""" + self.scope = parent_scope + @dataclass class CommentLine(ASTNode): @@ -224,7 +233,15 @@ class ParameterDeclaration(ASTNode): def __str__(self): return f"{self.name}{f' = {self.default}' if self.default else '' }" - + + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.name.build_scope(parent_scope) + if self.default: + # Default is evaluated in the caller scope (parent of parameter scope) + caller_scope = parent_scope.parent if parent_scope.parent else parent_scope + self.default.build_scope(caller_scope) + @dataclass class Argument(ASTNode): @@ -260,6 +277,10 @@ class PositionalArgument(Argument): def __str__(self): return f"{self.expr}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.expr.build_scope(parent_scope) + @dataclass class NamedArgument(Argument): @@ -283,6 +304,11 @@ class NamedArgument(Argument): def __str__(self): return f"{self.name}={self.expr}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.name.build_scope(parent_scope) + self.expr.build_scope(parent_scope) + @dataclass class RangeLiteral(Primary): @@ -309,6 +335,12 @@ class RangeLiteral(Primary): def __str__(self): return f"{self.start}:{self.end}:{self.step}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.start.build_scope(parent_scope) + self.end.build_scope(parent_scope) + self.step.build_scope(parent_scope) + @dataclass class Assignment(ASTNode): @@ -333,6 +365,16 @@ class Assignment(ASTNode): def __str__(self): return f"{self.name} = {self.expr}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.name.build_scope(parent_scope) + # Function literal bodies are closures that resolve variables lazily at + # call time, so the RHS always uses parent_scope (the full scope including + # the variable being assigned). This correctly handles recursive function + # literals and expressions containing function literals + # (e.g. `a = b ? function(x,n) a(...) : function(x,n) a(...)`). + self.expr.build_scope(parent_scope) + @dataclass class LetOp(Expression): @@ -356,6 +398,14 @@ class LetOp(Expression): def __str__(self): return f"let({', '.join(str(assignment) for assignment in self.assignments)}) {self.body}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + let_scope = parent_scope.child_scope() + for assignment in self.assignments: + let_scope.define_variable(assignment.name.name, assignment) + assignment.build_scope(let_scope) + self.body.build_scope(let_scope) + @dataclass class EchoOp(Expression): @@ -378,6 +428,12 @@ class EchoOp(Expression): def __str__(self): return f"echo({', '.join(str(arg) for arg in self.arguments)}) {self.body}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + for arg in self.arguments: + arg.build_scope(parent_scope) + self.body.build_scope(parent_scope) + @dataclass class AssertOp(Expression): @@ -400,6 +456,12 @@ class AssertOp(Expression): def __str__(self): return f"assert({', '.join(str(arg) for arg in self.arguments)}) {self.body}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + for arg in self.arguments: + arg.build_scope(parent_scope) + self.body.build_scope(parent_scope) + @dataclass class UnaryMinusOp(Expression): @@ -420,6 +482,10 @@ class UnaryMinusOp(Expression): def __str__(self): return f"-{self.expr}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.expr.build_scope(parent_scope) + @dataclass class AdditionOp(Expression): @@ -443,6 +509,11 @@ class AdditionOp(Expression): def __str__(self): return f"{self.left} + {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class SubtractionOp(Expression): @@ -466,6 +537,11 @@ class SubtractionOp(Expression): def __str__(self): return f"{self.left} - {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class MultiplicationOp(Expression): @@ -489,6 +565,11 @@ class MultiplicationOp(Expression): def __str__(self): return f"{self.left} * {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class DivisionOp(Expression): @@ -512,6 +593,11 @@ class DivisionOp(Expression): def __str__(self): return f"{self.left} / {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class ModuloOp(Expression): @@ -534,6 +620,11 @@ class ModuloOp(Expression): def __str__(self): return f"{self.left} % {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class ExponentOp(Expression): @@ -556,6 +647,11 @@ class ExponentOp(Expression): def __str__(self): return f"{self.left} ^ {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class BitwiseAndOp(Expression): @@ -578,6 +674,11 @@ class BitwiseAndOp(Expression): def __str__(self): return f"{self.left} & {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class BitwiseOrOp(Expression): @@ -600,6 +701,11 @@ class BitwiseOrOp(Expression): def __str__(self): return f"{self.left} | {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class BitwiseNotOp(Expression): @@ -619,6 +725,10 @@ class BitwiseNotOp(Expression): def __str__(self): return f"~{self.expr}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.expr.build_scope(parent_scope) + @dataclass class BitwiseShiftLeftOp(Expression): @@ -641,6 +751,11 @@ class BitwiseShiftLeftOp(Expression): def __str__(self): return f"{self.left} << {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class BitwiseShiftRightOp(Expression): @@ -663,6 +778,11 @@ class BitwiseShiftRightOp(Expression): def __str__(self): return f"{self.left} >> {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class LogicalAndOp(Expression): @@ -686,6 +806,11 @@ class LogicalAndOp(Expression): def __str__(self): return f"{self.left} && {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class LogicalOrOp(Expression): @@ -709,6 +834,11 @@ class LogicalOrOp(Expression): def __str__(self): return f"{self.left} || {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class LogicalNotOp(Expression): @@ -730,6 +860,10 @@ class LogicalNotOp(Expression): def __str__(self): return f"!{self.expr}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.expr.build_scope(parent_scope) + @dataclass class TernaryOp(Expression): @@ -755,6 +889,12 @@ class TernaryOp(Expression): def __str__(self): return f"{self.condition} ? {self.true_expr} : {self.false_expr}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.condition.build_scope(parent_scope) + self.true_expr.build_scope(parent_scope) + self.false_expr.build_scope(parent_scope) + @dataclass class EqualityOp(Expression): @@ -777,6 +917,11 @@ class EqualityOp(Expression): def __str__(self): return f"{self.left} == {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class InequalityOp(Expression): @@ -799,6 +944,11 @@ class InequalityOp(Expression): def __str__(self): return f"{self.left} != {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class GreaterThanOp(Expression): @@ -821,6 +971,11 @@ class GreaterThanOp(Expression): def __str__(self): return f"{self.left} > {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class GreaterThanOrEqualOp(Expression): @@ -843,6 +998,11 @@ class GreaterThanOrEqualOp(Expression): def __str__(self): return f"{self.left} >= {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class LessThanOp(Expression): @@ -865,6 +1025,11 @@ class LessThanOp(Expression): def __str__(self): return f"{self.left} < {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class LessThanOrEqualOp(Expression): @@ -887,6 +1052,11 @@ class LessThanOrEqualOp(Expression): def __str__(self): return f"{self.left} <= {self.right}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.right.build_scope(parent_scope) + @dataclass class FunctionLiteral(Expression): @@ -909,6 +1079,19 @@ class FunctionLiteral(Expression): def __str__(self): return f"function({', '.join(str(arg) for arg in self.arguments)}) {self.body}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + func_scope = parent_scope.child_scope() + # Normalize: builder may pass a single ParameterDeclaration for one-param literals + arguments = self.arguments + if not isinstance(arguments, (list, tuple)): + arguments = [arguments] if arguments is not None else [] + for arg in arguments: + if isinstance(arg, ParameterDeclaration): + func_scope.define_variable(arg.name.name, arg) + arg.build_scope(func_scope) + self.body.build_scope(func_scope) + @dataclass class PrimaryCall(Expression): @@ -933,6 +1116,12 @@ class PrimaryCall(Expression): def __str__(self): return f"{self.left}({', '.join(str(arg) for arg in self.arguments)})" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + for arg in self.arguments: + arg.build_scope(parent_scope) + @dataclass class PrimaryIndex(Expression): @@ -956,6 +1145,11 @@ class PrimaryIndex(Expression): def __str__(self): return f"{self.left}[{self.index}]" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.index.build_scope(parent_scope) + @dataclass class PrimaryMember(Expression): @@ -977,7 +1171,12 @@ class PrimaryMember(Expression): def __str__(self): return f"{self.left}.{self.member}" - + + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.left.build_scope(parent_scope) + self.member.build_scope(parent_scope) + @dataclass class VectorElement(ASTNode): @@ -989,7 +1188,7 @@ class VectorElement(ASTNode): """ def __str__(self): raise NotImplementedError - + @dataclass class ListCompLet(VectorElement): @@ -1010,7 +1209,19 @@ class ListCompLet(VectorElement): def __str__(self): return f"let ({self.assignments}) {self.body}" - + + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + let_scope = parent_scope.child_scope() + # Normalize: builder may pass a single Assignment for one-binding lets + assignments = self.assignments + if not isinstance(assignments, (list, tuple)): + assignments = [assignments] if assignments is not None else [] + for assignment in assignments: + let_scope.define_variable(assignment.name.name, assignment) + assignment.build_scope(let_scope) + self.body.build_scope(let_scope) + @dataclass class ListCompEach(VectorElement): @@ -1030,7 +1241,11 @@ class ListCompEach(VectorElement): def __str__(self): return f"each {self.body}" - + + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.body.build_scope(parent_scope) + @dataclass class ListCompFor(VectorElement): @@ -1053,7 +1268,22 @@ class ListCompFor(VectorElement): def __str__(self): return f"for ({self.assignments}) {self.body}" - + + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + for_scope = parent_scope.child_scope() + # Normalize: builder may pass a single Assignment for one-variable loops + assignments = self.assignments + if not isinstance(assignments, (list, tuple)): + assignments = [assignments] if assignments is not None else [] + for assignment in assignments: + assignment.scope = for_scope + for_scope.define_variable(assignment.name.name, assignment) + assignment.name.build_scope(for_scope) + # Loop range is evaluated in the enclosing scope + assignment.expr.build_scope(parent_scope) + self.body.build_scope(for_scope) + @dataclass class ListCompCFor(VectorElement): @@ -1079,7 +1309,25 @@ class ListCompCFor(VectorElement): def __str__(self): return f"for ({self.initial}; {self.condition}; {self.increment}) {self.body}" - + + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + for_scope = parent_scope.child_scope() + # Normalize: builder may pass single Assignments + initial = self.initial + if not isinstance(initial, (list, tuple)): + initial = [initial] if initial is not None else [] + increment = self.increment + if not isinstance(increment, (list, tuple)): + increment = [increment] if increment is not None else [] + for assignment in initial: + for_scope.define_variable(assignment.name.name, assignment) + assignment.build_scope(for_scope) + self.condition.build_scope(for_scope) + for assignment in increment: + assignment.build_scope(for_scope) + self.body.build_scope(for_scope) + @dataclass class ListCompIf(VectorElement): @@ -1102,6 +1350,11 @@ class ListCompIf(VectorElement): def __str__(self): return f"if {self.condition} {self.true_expr}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.condition.build_scope(parent_scope) + self.true_expr.build_scope(parent_scope) + @dataclass class ListCompIfElse(VectorElement): @@ -1126,6 +1379,12 @@ class ListCompIfElse(VectorElement): def __str__(self): return f"if {self.condition} {self.true_expr} else {self.false_expr}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.condition.build_scope(parent_scope) + self.true_expr.build_scope(parent_scope) + self.false_expr.build_scope(parent_scope) + @dataclass class ListComprehension(Expression): @@ -1147,7 +1406,12 @@ class ListComprehension(Expression): def __str__(self): return f"[{', '.join(str(element) for element in self.elements)}]" - + + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + for elem in self.elements: + elem.build_scope(parent_scope) + @dataclass class ModuleInstantiation(ASTNode): @@ -1183,7 +1447,18 @@ class ModularCall(ModuleInstantiation): def __str__(self): return f"{self.name}({', '.join(str(arg) for arg in self.arguments)})" - + + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.name.build_scope(parent_scope) + for arg in self.arguments: + arg.build_scope(parent_scope) + if self.children: + children_scope = parent_scope.child_scope() + _collect_hoisted_declarations(self.children, children_scope) + for child in self.children: + child.build_scope(children_scope) + @dataclass class ModularFor(ModuleInstantiation): @@ -1205,7 +1480,21 @@ class ModularFor(ModuleInstantiation): def __str__(self): return f"for ({', '.join(str(assignment) for assignment in self.assignments)}) {self.body}" - + + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + for_scope = parent_scope.child_scope() + for assignment in self.assignments: + assignment.scope = for_scope + for_scope.define_variable(assignment.name.name, assignment) + assignment.name.build_scope(for_scope) + # Loop range is evaluated in the enclosing scope + assignment.expr.build_scope(parent_scope) + body = self.body if isinstance(self.body, list) else [self.body] + _collect_hoisted_declarations(body, for_scope) + for node in body: + node.build_scope(for_scope) + @dataclass class ModularCFor(ModuleInstantiation): @@ -1231,7 +1520,21 @@ class ModularCFor(ModuleInstantiation): def __str__(self): return f"for ({'; '.join(str(a) for a in self.initial)}; {self.condition}; {', '.join(str(a) for a in self.increment)}) {self.body}" - + + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + for_scope = parent_scope.child_scope() + for assignment in self.initial: + for_scope.define_variable(assignment.name.name, assignment) + assignment.build_scope(for_scope) + self.condition.build_scope(for_scope) + for assignment in self.increment: + assignment.build_scope(for_scope) + body = self.body if isinstance(self.body, list) else [self.body] + _collect_hoisted_declarations(body, for_scope) + for node in body: + node.build_scope(for_scope) + @dataclass class ModularIntersectionFor(ModuleInstantiation): @@ -1252,7 +1555,21 @@ class ModularIntersectionFor(ModuleInstantiation): def __str__(self): return f"intersection_for ({', '.join(str(assignment) for assignment in self.assignments)}) {self.body}" - + + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + for_scope = parent_scope.child_scope() + for assignment in self.assignments: + assignment.scope = for_scope + for_scope.define_variable(assignment.name.name, assignment) + assignment.name.build_scope(for_scope) + assignment.expr.build_scope(parent_scope) + body = self.body if isinstance(self.body, list) else [self.body] + _collect_hoisted_declarations(body, for_scope) + for node in body: + node.build_scope(for_scope) + + @dataclass class ModularIntersectionCFor(ModuleInstantiation): """Represents an intersection_for C-style loop module instantiation. @@ -1284,6 +1601,20 @@ def __str__(self): f") {self.body}" ) + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + for_scope = parent_scope.child_scope() + for assignment in self.initial: + for_scope.define_variable(assignment.name.name, assignment) + assignment.build_scope(for_scope) + self.condition.build_scope(for_scope) + for assignment in self.increment: + assignment.build_scope(for_scope) + body = self.body if isinstance(self.body, list) else [self.body] + _collect_hoisted_declarations(body, for_scope) + for node in body: + node.build_scope(for_scope) + @dataclass class ModularLet(ModuleInstantiation): @@ -1307,7 +1638,17 @@ class ModularLet(ModuleInstantiation): def __str__(self): return f"let ({', '.join(str(assignment) for assignment in self.assignments)}) {', '.join(str(child) for child in self.children)}" - + + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + let_scope = parent_scope.child_scope() + for assignment in self.assignments: + let_scope.define_variable(assignment.name.name, assignment) + assignment.build_scope(let_scope) + _collect_hoisted_declarations(self.children, let_scope) + for child in self.children: + child.build_scope(let_scope) + @dataclass class ModularEcho(ModuleInstantiation): @@ -1329,7 +1670,17 @@ class ModularEcho(ModuleInstantiation): def __str__(self): return f"echo({', '.join(str(arg) for arg in self.arguments)}) {', '.join(str(child) for child in self.children)}" - + + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + for arg in self.arguments: + arg.build_scope(parent_scope) + if self.children: + children_scope = parent_scope.child_scope() + _collect_hoisted_declarations(self.children, children_scope) + for child in self.children: + child.build_scope(children_scope) + @dataclass class ModularAssert(ModuleInstantiation): @@ -1351,7 +1702,17 @@ class ModularAssert(ModuleInstantiation): def __str__(self): return f"assert({', '.join(str(arg) for arg in self.arguments)}) {', '.join(str(child) for child in self.children)}" - + + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + for arg in self.arguments: + arg.build_scope(parent_scope) + if self.children: + children_scope = parent_scope.child_scope() + _collect_hoisted_declarations(self.children, children_scope) + for child in self.children: + child.build_scope(children_scope) + @dataclass class ModularIf(ModuleInstantiation): @@ -1374,6 +1735,15 @@ class ModularIf(ModuleInstantiation): def __str__(self): return f"if ({self.condition}) {self.true_branch}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.condition.build_scope(parent_scope) + true_scope = parent_scope.child_scope() + branch = self.true_branch if isinstance(self.true_branch, list) else [self.true_branch] + _collect_hoisted_declarations(branch, true_scope) + for node in branch: + node.build_scope(true_scope) + @dataclass class ModularIfElse(ModuleInstantiation): @@ -1397,6 +1767,20 @@ class ModularIfElse(ModuleInstantiation): def __str__(self): return f"if ({self.condition}) {self.true_branch} else {self.false_branch}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.condition.build_scope(parent_scope) + true_scope = parent_scope.child_scope() + true_branch = self.true_branch if isinstance(self.true_branch, list) else [self.true_branch] + _collect_hoisted_declarations(true_branch, true_scope) + for node in true_branch: + node.build_scope(true_scope) + false_scope = parent_scope.child_scope() + false_branch = self.false_branch if isinstance(self.false_branch, list) else [self.false_branch] + _collect_hoisted_declarations(false_branch, false_scope) + for node in false_branch: + node.build_scope(false_scope) + @dataclass class ModularModifierShowOnly(ModuleInstantiation): @@ -1417,6 +1801,10 @@ class ModularModifierShowOnly(ModuleInstantiation): def __str__(self): return f"!{self.child}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.child.build_scope(parent_scope) + @dataclass class ModularModifierHighlight(ModuleInstantiation): """Represents the '#' (highlight) module modifier. @@ -1436,6 +1824,10 @@ class ModularModifierHighlight(ModuleInstantiation): def __str__(self): return f"#{self.child}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.child.build_scope(parent_scope) + @dataclass class ModularModifierBackground(ModuleInstantiation): @@ -1457,6 +1849,10 @@ class ModularModifierBackground(ModuleInstantiation): def __str__(self): return f"%{self.child}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.child.build_scope(parent_scope) + @dataclass class ModularModifierDisable(ModuleInstantiation): @@ -1477,6 +1873,10 @@ class ModularModifierDisable(ModuleInstantiation): def __str__(self): return f"*{self.child}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.child.build_scope(parent_scope) + @dataclass class ModuleDeclaration(ASTNode): @@ -1494,17 +1894,29 @@ class ModuleDeclaration(ASTNode): Attributes: name: The module name as an Identifier. parameters: List of parameter declarations. - children: List of module instantiations in the module body. + children: List of statements in the module body (module instantiations, + assignments, function and module declarations) for scoping and hoisting. """ name: Identifier parameters: list[ParameterDeclaration] - children: list[ModuleInstantiation] + children: list[ModuleInstantiation | Assignment | FunctionDeclaration | ModuleDeclaration] def __str__(self): params = ', '.join(str(param) for param in self.parameters) children = ', '.join(str(child) for child in self.children) return f"module {self.name}({params}) {{ {children} }}" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.name.build_scope(parent_scope) + mod_scope = parent_scope.child_scope() + for param in self.parameters: + mod_scope.define_variable(param.name.name, param) + param.build_scope(mod_scope) + _collect_hoisted_declarations(self.children, mod_scope) + for child in self.children: + child.build_scope(mod_scope) + @dataclass class FunctionDeclaration(ASTNode): @@ -1531,6 +1943,15 @@ def __str__(self): params = ', '.join(str(param) for param in self.parameters) return f"function {self.name}({params}) = {self.expr};" + def build_scope(self, parent_scope: "Scope") -> None: + self.scope = parent_scope + self.name.build_scope(parent_scope) + func_scope = parent_scope.child_scope() + for param in self.parameters: + func_scope.define_variable(param.name.name, param) + param.build_scope(func_scope) + self.expr.build_scope(func_scope) + @dataclass class UseStatement(ASTNode): @@ -1574,3 +1995,17 @@ def __str__(self): return f"include <{self.filepath.val}>" +def _collect_hoisted_declarations(nodes, scope: "Scope") -> None: + """Scan a list of nodes and register assignments, functions, and modules in scope. + + OpenSCAD hoists variable, function, and module declarations within block + scopes, making them visible to all sibling nodes regardless of textual + order. Call this before calling build_scope() on the same node list. + """ + for node in nodes: + if isinstance(node, Assignment): + scope.define_variable(node.name.name, node) + elif isinstance(node, FunctionDeclaration): + scope.define_function(node.name.name, node) + elif isinstance(node, ModuleDeclaration): + scope.define_module(node.name.name, node) diff --git a/src/openscad_parser/ast/scope.py b/src/openscad_parser/ast/scope.py new file mode 100644 index 0000000..9556840 --- /dev/null +++ b/src/openscad_parser/ast/scope.py @@ -0,0 +1,122 @@ +"""Scope tracking for OpenSCAD AST nodes. + +This module provides the Scope class for representing lexical scopes in +OpenSCAD ASTs, and the build_scopes() convenience function that drives +scope population by calling build_scope() on each top-level AST node. +""" +from __future__ import annotations +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, List, Optional + +if TYPE_CHECKING: + from .nodes import ( + ASTNode, Assignment, + FunctionDeclaration, + ModuleDeclaration, + ParameterDeclaration, + ) + + +@dataclass +class Scope: + """Represents a lexical scope in OpenSCAD. + + A scope tracks variable, function, and module bindings that are visible + at a particular location in the AST. Scopes form a tree structure through + parent references, enabling lookup to traverse from inner to outer scopes. + + OpenSCAD has three separate namespaces: + - Variables: Assignments and parameter declarations + - Functions: FunctionDeclaration nodes + - Modules: ModuleDeclaration nodes + + The same name can exist in all three namespaces simultaneously. + + Attributes: + parent: The enclosing (parent) scope, or None for the root scope. + variables: Variables defined in this scope (name -> declaring node). + functions: Functions defined in this scope (name -> FunctionDeclaration). + modules: Modules defined in this scope (name -> ModuleDeclaration). + """ + parent: Optional["Scope"] = None + variables: dict[str, "Assignment | ParameterDeclaration"] = field(default_factory=dict) + functions: dict[str, "FunctionDeclaration"] = field(default_factory=dict) + modules: dict[str, "ModuleDeclaration"] = field(default_factory=dict) + + def lookup_variable(self, name: str) -> Optional["Assignment | ParameterDeclaration"]: + """Look up a variable by name, searching parent scopes.""" + if name in self.variables: + return self.variables[name] + if self.parent: + return self.parent.lookup_variable(name) + return None + + def lookup_function(self, name: str) -> Optional["FunctionDeclaration"]: + """Look up a function by name, searching parent scopes.""" + if name in self.functions: + return self.functions[name] + if self.parent: + return self.parent.lookup_function(name) + return None + + def lookup_module(self, name: str) -> Optional["ModuleDeclaration"]: + """Look up a module by name, searching parent scopes.""" + if name in self.modules: + return self.modules[name] + if self.parent: + return self.parent.lookup_module(name) + return None + + def define_variable(self, name: str, node: "Assignment | ParameterDeclaration") -> None: + """Define a variable in this scope.""" + self.variables[name] = node + + def define_function(self, name: str, node: "FunctionDeclaration") -> None: + """Define a function in this scope.""" + self.functions[name] = node + + def define_module(self, name: str, node: "ModuleDeclaration") -> None: + """Define a module in this scope.""" + self.modules[name] = node + + def child_scope(self) -> "Scope": + """Create a new child scope with this scope as parent.""" + return Scope(parent=self) + + def __repr__(self) -> str: + vars_str = ", ".join(self.variables.keys()) if self.variables else "none" + funcs_str = ", ".join(self.functions.keys()) if self.functions else "none" + mods_str = ", ".join(self.modules.keys()) if self.modules else "none" + parent_str = "has parent" if self.parent else "root" + return f"" + + +def build_scopes(ast: List["ASTNode"]) -> Scope: + """Build scope tree for a list of top-level AST nodes. + + Creates a root scope, hoists top-level declarations into it, then calls + build_scope() on each node so every node in the tree gets its scope set. + + Args: + ast: List of top-level AST nodes. + + Returns: + The root scope containing top-level bindings. + """ + from .nodes import Assignment, FunctionDeclaration, ModuleDeclaration + + root_scope = Scope() + + # Hoist top-level declarations so all siblings can see each other + for node in ast: + if isinstance(node, Assignment): + root_scope.define_variable(node.name.name, node) + elif isinstance(node, FunctionDeclaration): + root_scope.define_function(node.name.name, node) + elif isinstance(node, ModuleDeclaration): + root_scope.define_module(node.name.name, node) + + for node in ast: + node.build_scope(root_scope) + + return root_scope diff --git a/src/openscad_parser/ast/serialization.py b/src/openscad_parser/ast/serialization.py index e61836b..541c3e0 100644 --- a/src/openscad_parser/ast/serialization.py +++ b/src/openscad_parser/ast/serialization.py @@ -210,9 +210,10 @@ def _serialize_node(node: ASTNode, include_position: bool) -> dict[str, Any]: if include_position: result["_position"] = _serialize_position(node.position) - # Get all fields from the dataclass (excluding 'position' which we handle specially) + # Get all fields from the dataclass (excluding 'position' which we handle specially, + # and 'scope' which is runtime metadata not suitable for serialization) for field in dataclasses.fields(node): - if field.name == "position": + if field.name in ("position", "scope"): continue value = getattr(node, field.name) result[field.name] = _serialize_value(value, include_position) diff --git a/tests/test_ast_generation.py b/tests/test_ast_generation.py index 59a3819..2ead5b6 100644 --- a/tests/test_ast_generation.py +++ b/tests/test_ast_generation.py @@ -1151,3 +1151,213 @@ def test_complex_module_ast(self, parser): assert isinstance(module, ModuleDeclaration) assert len(module.children) >= 2 # Should have cube and sphere calls + +def _expr(parser, code: str): + """Parse ``x = ;`` and return the RHS expression node.""" + ast = parse_ast(parser, f"x = {code};") + assert ast is not None and len(ast) == 1 + node = ast[0] + assert isinstance(node, Assignment) + return node.expr + + +class TestLogicalNotAST: + """AST-level tests for the logical NOT operator (!).""" + + def test_not_true(self, parser): + """! applied to boolean literal true.""" + expr = _expr(parser, "!true") + assert isinstance(expr, LogicalNotOp) + assert isinstance(expr.expr, BooleanLiteral) + assert expr.expr.val is True + + def test_not_false(self, parser): + """! applied to boolean literal false.""" + expr = _expr(parser, "!false") + assert isinstance(expr, LogicalNotOp) + assert isinstance(expr.expr, BooleanLiteral) + assert expr.expr.val is False + + def test_not_identifier(self, parser): + """! applied to a variable.""" + expr = _expr(parser, "!a") + assert isinstance(expr, LogicalNotOp) + assert isinstance(expr.expr, Identifier) + assert expr.expr.name == "a" + + def test_not_double(self, parser): + """!! is LogicalNotOp wrapping LogicalNotOp.""" + expr = _expr(parser, "!!a") + assert isinstance(expr, LogicalNotOp) + assert isinstance(expr.expr, LogicalNotOp) + assert isinstance(expr.expr.expr, Identifier) + + def test_not_equality(self, parser): + """! applied to a parenthesized equality expression.""" + expr = _expr(parser, "!(a == b)") + assert isinstance(expr, LogicalNotOp) + assert isinstance(expr.expr, EqualityOp) + + def test_not_inequality(self, parser): + """! applied to a parenthesized inequality expression.""" + expr = _expr(parser, "!(a != b)") + assert isinstance(expr, LogicalNotOp) + assert isinstance(expr.expr, InequalityOp) + + def test_not_comparison(self, parser): + """! applied to a parenthesized greater-than comparison.""" + expr = _expr(parser, "!(a > 0)") + assert isinstance(expr, LogicalNotOp) + assert isinstance(expr.expr, GreaterThanOp) + + def test_not_binds_tighter_than_logical_and(self, parser): + """!a && b is ((!a) && b), not !(a && b).""" + expr = _expr(parser, "!a && b") + assert isinstance(expr, LogicalAndOp) + assert isinstance(expr.left, LogicalNotOp) + assert expr.left.expr.name == "a" + assert isinstance(expr.right, Identifier) + assert expr.right.name == "b" + + def test_not_both_sides_of_and(self, parser): + """!a && !b — both AND operands are negated.""" + expr = _expr(parser, "!a && !b") + assert isinstance(expr, LogicalAndOp) + assert isinstance(expr.left, LogicalNotOp) + assert isinstance(expr.right, LogicalNotOp) + + def test_not_binds_tighter_than_logical_or(self, parser): + """!a || b is ((!a) || b).""" + expr = _expr(parser, "!a || b") + assert isinstance(expr, LogicalOrOp) + assert isinstance(expr.left, LogicalNotOp) + assert isinstance(expr.right, Identifier) + + def test_not_in_ternary_condition(self, parser): + """!a used as ternary condition.""" + expr = _expr(parser, "!a ? 1 : 2") + assert isinstance(expr, TernaryOp) + assert isinstance(expr.condition, LogicalNotOp) + + def test_not_in_ternary_branch(self, parser): + """!b in the true branch of a ternary.""" + expr = _expr(parser, "a ? !b : c") + assert isinstance(expr, TernaryOp) + assert isinstance(expr.true_expr, LogicalNotOp) + + def test_not_in_if_condition(self, parser): + """! used as a condition in an if statement.""" + ast = parse_ast(parser, "if (!cond) cube();") + assert ast is not None and len(ast) == 1 + node = ast[0] + assert isinstance(node, ModularIf) + assert isinstance(node.condition, LogicalNotOp) + assert isinstance(node.condition.expr, Identifier) + assert node.condition.expr.name == "cond" + + def test_str_simple(self, parser): + """__str__ renders !true as !True.""" + expr = _expr(parser, "!true") + assert str(expr) == "!True" + + def test_str_double(self, parser): + """__str__ renders !!a as !!a.""" + expr = _expr(parser, "!!a") + assert str(expr) == "!!a" + + +class TestBitwiseNotAST: + """AST-level tests for the bitwise NOT operator (~).""" + + def test_not_number(self, parser): + """~ applied to a number literal.""" + expr = _expr(parser, "~5") + assert isinstance(expr, BitwiseNotOp) + assert isinstance(expr.expr, NumberLiteral) + assert expr.expr.val == 5 + + def test_not_identifier(self, parser): + """~ applied to a variable.""" + expr = _expr(parser, "~a") + assert isinstance(expr, BitwiseNotOp) + assert isinstance(expr.expr, Identifier) + assert expr.expr.name == "a" + + def test_not_double(self, parser): + """~~ is BitwiseNotOp wrapping BitwiseNotOp.""" + expr = _expr(parser, "~~a") + assert isinstance(expr, BitwiseNotOp) + assert isinstance(expr.expr, BitwiseNotOp) + assert isinstance(expr.expr.expr, Identifier) + + def test_not_parenthesized_addition(self, parser): + """~ applied to a parenthesized addition.""" + expr = _expr(parser, "~(a + b)") + assert isinstance(expr, BitwiseNotOp) + assert isinstance(expr.expr, AdditionOp) + + def test_not_parenthesized_shift(self, parser): + """~ applied to a parenthesized left-shift expression.""" + expr = _expr(parser, "~(a << b)") + assert isinstance(expr, BitwiseNotOp) + assert isinstance(expr.expr, BitwiseShiftLeftOp) + + def test_not_binds_tighter_than_bitwise_and(self, parser): + """~a & b is ((~a) & b), not ~(a & b).""" + expr = _expr(parser, "~a & b") + assert isinstance(expr, BitwiseAndOp) + assert isinstance(expr.left, BitwiseNotOp) + assert expr.left.expr.name == "a" + assert isinstance(expr.right, Identifier) + assert expr.right.name == "b" + + def test_not_both_sides_of_and(self, parser): + """~a & ~b — both AND operands are complemented.""" + expr = _expr(parser, "~a & ~b") + assert isinstance(expr, BitwiseAndOp) + assert isinstance(expr.left, BitwiseNotOp) + assert isinstance(expr.right, BitwiseNotOp) + + def test_not_binds_tighter_than_bitwise_or(self, parser): + """~a | b is ((~a) | b).""" + expr = _expr(parser, "~a | b") + assert isinstance(expr, BitwiseOrOp) + assert isinstance(expr.left, BitwiseNotOp) + assert isinstance(expr.right, Identifier) + + def test_str_simple(self, parser): + """__str__ renders ~5 as ~5.0 (numbers are stored as floats).""" + expr = _expr(parser, "~5") + assert str(expr) == "~5.0" + + def test_str_double(self, parser): + """__str__ renders ~~a as ~~a.""" + expr = _expr(parser, "~~a") + assert str(expr) == "~~a" + + +class TestMixedNotOperatorsAST: + """AST-level tests for combinations of ! and ~ in the same expression.""" + + def test_bitwise_not_of_logical_not(self, parser): + """~!a — BitwiseNotOp wrapping LogicalNotOp.""" + expr = _expr(parser, "~!a") + assert isinstance(expr, BitwiseNotOp) + assert isinstance(expr.expr, LogicalNotOp) + assert isinstance(expr.expr.expr, Identifier) + + def test_logical_not_of_bitwise_not(self, parser): + """!~a — LogicalNotOp wrapping BitwiseNotOp.""" + expr = _expr(parser, "!~a") + assert isinstance(expr, LogicalNotOp) + assert isinstance(expr.expr, BitwiseNotOp) + assert isinstance(expr.expr.expr, Identifier) + + def test_three_levels_of_nesting(self, parser): + """!~!a — three alternating levels of not operators.""" + expr = _expr(parser, "!~!a") + assert isinstance(expr, LogicalNotOp) + assert isinstance(expr.expr, BitwiseNotOp) + assert isinstance(expr.expr.expr, LogicalNotOp) + assert isinstance(expr.expr.expr.expr, Identifier) + diff --git a/tests/test_expressions.py b/tests/test_expressions.py index 13df012..b122cf4 100644 --- a/tests/test_expressions.py +++ b/tests/test_expressions.py @@ -326,5 +326,3 @@ def test_complex_expression_5(self, parser): """Test complex expression with ternary.""" code = "x = a > b ? c + d : e - f;" parse_success(parser, code) - - diff --git a/tests/test_scope.py b/tests/test_scope.py new file mode 100644 index 0000000..e247f86 --- /dev/null +++ b/tests/test_scope.py @@ -0,0 +1,716 @@ +"""Tests for scope tracking functionality.""" +import pytest +from openscad_parser.ast import ( + getASTfromString, build_scopes, Scope, + Assignment, FunctionDeclaration, ModuleDeclaration, + ModularCall, ModularFor, ModularIf, ModularIfElse, + ModularLet, LetOp, FunctionLiteral, Identifier, + ModularCFor, ModularEcho, ModularAssert, + ModularModifierShowOnly, ModularModifierHighlight, + ModularModifierBackground, ModularModifierDisable, + ListComprehension, ListCompFor, ListCompCFor, ListCompLet, + ListCompIf, ListCompIfElse, ListCompEach, +) + + +class TestScopeBasics: + """Test basic Scope class functionality.""" + + def test_empty_scope(self): + scope = Scope() + assert scope.parent is None + assert scope.variables == {} + assert scope.functions == {} + assert scope.modules == {} + + def test_lookup_variable_not_found(self): + scope = Scope() + assert scope.lookup_variable("x") is None + + def test_lookup_function_not_found(self): + scope = Scope() + assert scope.lookup_function("foo") is None + + def test_lookup_module_not_found(self): + scope = Scope() + assert scope.lookup_module("bar") is None + + def test_child_scope(self): + parent = Scope() + child = parent.child_scope() + assert child.parent is parent + + def test_repr(self): + scope = Scope() + repr_str = repr(scope) + assert "root" in repr_str + + def test_define_and_lookup_variable(self): + scope = Scope() + ast = getASTfromString("a = 1;") + assert ast is not None and isinstance(ast, list) + n = ast[0] + assert isinstance(n, Assignment) + scope.define_variable("a", n) + assert scope.lookup_variable("a") is n + + def test_define_and_lookup_function(self): + scope = Scope() + ast = getASTfromString("function f(x) = x;") + assert ast is not None and isinstance(ast, list) + n = ast[0] + assert isinstance(n, FunctionDeclaration) + scope.define_function("f", n) + assert scope.lookup_function("f") is n + + def test_define_and_lookup_module(self): + scope = Scope() + ast = getASTfromString("module m() {}") + assert ast is not None and isinstance(ast, list) + n = ast[0] + assert isinstance(n, ModuleDeclaration) + scope.define_module("m", n) + assert scope.lookup_module("m") is n + + def test_lookup_variable_in_parent(self): + parent = Scope() + ast = getASTfromString("x = 1;") + assert ast is not None and isinstance(ast, list) + n = ast[0] + assert isinstance(n, Assignment) + parent.define_variable("x", n) + child = parent.child_scope() + assert child.lookup_variable("x") is not None + + def test_lookup_function_in_parent(self): + parent = Scope() + ast = getASTfromString("function f() = 1;") + assert ast is not None and isinstance(ast, list) + n = ast[0] + assert isinstance(n, FunctionDeclaration) + parent.define_function("f", n) + child = parent.child_scope() + assert child.lookup_function("f") is not None + + def test_lookup_module_in_parent(self): + parent = Scope() + ast = getASTfromString("module m() {}") + assert ast is not None and isinstance(ast, list) + n = ast[0] + assert isinstance(n, ModuleDeclaration) + parent.define_module("m", n) + child = parent.child_scope() + assert child.lookup_module("m") is not None + + def test_repr_with_bindings(self): + scope = Scope() + ast = getASTfromString("x = 1;") + assert ast is not None and isinstance(ast, list) + n = ast[0] + assert isinstance(n, Assignment) + scope.define_variable("x", n) + repr_str = repr(scope) + assert "x" in repr_str and "vars=" in repr_str + + +class TestScopeBuilderBasics: + """Test basic scope-building functionality.""" + + def test_build_scopes_convenience(self): + """build_scopes() returns root scope and attaches scopes to nodes.""" + ast = getASTfromString("a = 1;") + assert ast is not None and isinstance(ast, list) + root = build_scopes(ast) + assert isinstance(root, Scope) + assert root.lookup_variable("a") is not None + assert ast[0].scope is not None # type: ignore + + def test_empty_ast(self): + root = build_scopes([]) + assert isinstance(root, Scope) + assert root.parent is None + + def test_simple_assignment(self): + ast = getASTfromString("x = 10;") + assert ast is not None and isinstance(ast, list) + root = build_scopes(ast) + + # Variable should be in root scope + var = root.lookup_variable("x") + assert var is not None + assert isinstance(var, Assignment) + + def test_assignment_scope_attached(self): + ast = getASTfromString("x = 10;") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + + # Each node should have scope attached + assignment = ast[0] + assert hasattr(assignment, 'scope') + assert assignment.scope is not None # type: ignore + + def test_multiple_assignments(self): + ast = getASTfromString("x = 10; y = 20; z = 30;") + assert ast is not None and isinstance(ast, list) + root = build_scopes(ast) + + assert root.lookup_variable("x") is not None + assert root.lookup_variable("y") is not None + assert root.lookup_variable("z") is not None + + +class TestFunctionScope: + """Test function declaration scoping.""" + + def test_function_in_root_scope(self): + ast = getASTfromString("function foo(a) = a + 1;") + assert ast is not None and isinstance(ast, list) + root = build_scopes(ast) + + func = root.lookup_function("foo") + assert func is not None + assert isinstance(func, FunctionDeclaration) + + def test_function_parameters_in_function_scope(self): + ast = getASTfromString("function foo(a, b) = a + b;") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + + func_decl = ast[0] + # The expression (a + b) should have access to parameters + expr = func_decl.expr # type: ignore + assert expr.scope is not None + assert expr.scope.lookup_variable("a") is not None + assert expr.scope.lookup_variable("b") is not None + + def test_function_sees_outer_variables(self): + ast = getASTfromString("x = 10; function foo(a) = a + x;") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + + func_decl = ast[1] + expr = func_decl.expr # type: ignore + # Function should see outer variable x + assert expr.scope.lookup_variable("x") is not None + + def test_function_parameter_with_default(self): + """Parameter with default is in function scope; default expr visited in caller scope.""" + ast = getASTfromString("function foo(x, y = 2) = x + y;") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + func_decl = ast[0] + assert func_decl.expr.scope.lookup_variable("x") is not None # type: ignore + assert func_decl.expr.scope.lookup_variable("y") is not None # type: ignore + + def test_function_parameter_default_visited_in_caller_scope(self): + """ParameterDeclaration with default: node.default is visited in caller (parent) scope.""" + ast = getASTfromString("function foo(x = 1) = x;") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + func_decl = ast[0] + param = func_decl.parameters[0] # type: ignore + assert param.default is not None + # Default expr is visited in scope.parent; should have a scope attached + assert param.default.scope is not None # type: ignore + + +class TestModuleScope: + """Test module declaration scoping.""" + + def test_module_in_root_scope(self): + ast = getASTfromString("module foo() { cube(1); }") + assert ast is not None and isinstance(ast, list) + root = build_scopes(ast) + + mod = root.lookup_module("foo") + assert mod is not None + assert isinstance(mod, ModuleDeclaration) + + def test_module_parameters_in_module_scope(self): + ast = getASTfromString("module foo(size) { cube(size); }") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + + mod_decl = ast[0] + # Children should have access to parameters + if mod_decl.children: # type: ignore + child = mod_decl.children[0] # type: ignore + assert child.scope.lookup_variable("size") is not None + + def test_module_parameter_with_default(self): + """Module with default parameter: default expr is visited in caller scope.""" + ast = getASTfromString("module foo(size = 1) { cube(size); }") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + mod_decl = ast[0] + assert len(mod_decl.children) >= 1 # type: ignore + assert mod_decl.children[0].scope.lookup_variable("size") is not None # type: ignore + + def test_nested_function_in_module(self): + ast = getASTfromString(""" + module outer() { + function helper(x) = x * 2; + cube(helper(5)); + } + """) + assert ast is not None and isinstance(ast, list) + root = build_scopes(ast) + + # helper should NOT be in root scope + assert root.lookup_function("helper") is None + + # But should be in module's scope + mod_decl = ast[0] + if mod_decl.children: # type: ignore + child = mod_decl.children[0] # type: ignore + assert child.scope.lookup_function("helper") is not None + + +class TestHoisting: + """Test hoisting behavior in modular scopes.""" + + def test_assignment_hoisted_in_module(self): + ast = getASTfromString(""" + module foo() { + cube(x); + x = 10; + } + """) + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + + mod_decl = ast[0] + # cube(x) should see x due to hoisting + if mod_decl.children: # type: ignore + cube_call = mod_decl.children[0] # type: ignore + assert cube_call.scope.lookup_variable("x") is not None + + +class TestLetExpressions: + """Test let expression scoping.""" + + def test_let_op_creates_scope(self): + ast = getASTfromString("x = let(a=1, b=2) a + b;") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + + # The let expression should create a scope with a and b + assignment = ast[0] + let_op = assignment.expr # type: ignore + assert isinstance(let_op, LetOp) + + # The body should have access to let variables + body = let_op.body + assert hasattr(body, 'scope') + assert body.scope.lookup_variable("a") is not None # type: ignore + assert body.scope.lookup_variable("b") is not None # type: ignore + + def test_let_variables_not_in_outer_scope(self): + ast = getASTfromString("x = let(a=1) a; y = 2;") + assert ast is not None and isinstance(ast, list) + root = build_scopes(ast) + + # a should NOT be in root scope + assert root.lookup_variable("a") is None + + +class TestModularConstructs: + """Test modular construct scoping (for, if, etc.).""" + + def test_modular_for_creates_scope(self): + ast = getASTfromString("for (i = [1:10]) cube(i);") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + + for_node = ast[0] + assert isinstance(for_node, ModularFor) + + # Loop body should have access to loop variable + body = for_node.body + if isinstance(body, list): + body = body[0] + assert hasattr(body, 'scope') + assert body.scope.lookup_variable("i") is not None # type: ignore + + def test_modular_if_creates_scope(self): + ast = getASTfromString("if (true) { x = 10; cube(x); }") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + + if_node = ast[0] + assert isinstance(if_node, ModularIf) + + def test_modular_let_creates_scope(self): + ast = getASTfromString("let(x=10) cube(x);") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + + let_node = ast[0] + assert isinstance(let_node, ModularLet) + + def test_modular_if_single_branch(self): + """ModularIf with single statement (non-list true_branch).""" + ast = getASTfromString("if (true) cube(1);") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + if_node = ast[0] + assert isinstance(if_node, ModularIf) + assert if_node.true_branch.scope is not None # type: ignore + + def test_modular_if_else_single_branches(self): + """ModularIfElse with single-statement branches.""" + ast = getASTfromString("if (true) cube(1); else sphere(1);") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + if_node = ast[0] + assert isinstance(if_node, ModularIfElse) + assert if_node.true_branch.scope is not None # type: ignore + assert if_node.false_branch.scope is not None # type: ignore + + def test_modular_for_list_body(self): + """ModularFor with statement block (list body) or single statement.""" + ast = getASTfromString("for (i = [1:3]) { cube(i); sphere(i); }") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + for_node = ast[0] + assert isinstance(for_node, ModularFor) + body = for_node.body + first = body[0] if isinstance(body, list) else body + assert first.scope.lookup_variable("i") is not None # type: ignore + + def test_modular_c_for(self): + """ModularCFor creates scope with loop variable.""" + ast = getASTfromString("for (i = 0; i < 3; i = i + 1) cube(i);") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + for_node = ast[0] + assert isinstance(for_node, ModularCFor) + assert for_node.body.scope.lookup_variable("i") is not None # type: ignore + + def test_modular_c_for_block_body(self): + """ModularCFor with statement block (list body).""" + ast = getASTfromString("for (i = 0; i < 3; i = i + 1) { cube(i); sphere(i); }") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + for_node = ast[0] + assert isinstance(for_node, ModularCFor) + body = for_node.body + first = body[0] if isinstance(body, list) else body + assert first.scope.lookup_variable("i") is not None # type: ignore + + def test_modular_echo_with_children(self): + """ModularEcho with child statement gets children scope.""" + ast = getASTfromString('echo("ok") cube(1);') + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + echo_node = ast[0] + assert isinstance(echo_node, ModularEcho) + assert len(echo_node.children) >= 1 + assert echo_node.children[0].scope is not None # type: ignore + + def test_modular_assert_with_children(self): + """ModularAssert with child statement gets children scope.""" + ast = getASTfromString("assert(true) cube(1);") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + assert_node = ast[0] + assert isinstance(assert_node, ModularAssert) + assert len(assert_node.children) >= 1 + assert assert_node.children[0].scope is not None # type: ignore + + def test_modular_call_empty_children(self): + """ModularCall with empty block: if node.children is falsy, skip children scope.""" + ast = getASTfromString("cube(1) { }") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + call = ast[0] + assert isinstance(call, ModularCall) + assert len(call.children) == 0 + + def test_modifier_show_only(self): + ast = getASTfromString("! cube(1);") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + mod = ast[0] + assert isinstance(mod, ModularModifierShowOnly) + assert mod.child.scope is not None # type: ignore + + def test_modifier_highlight(self): + ast = getASTfromString("# sphere(1);") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + mod = ast[0] + assert isinstance(mod, ModularModifierHighlight) + assert mod.child.scope is not None # type: ignore + + def test_modifier_background(self): + ast = getASTfromString("% cube(1);") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + mod = ast[0] + assert isinstance(mod, ModularModifierBackground) + assert mod.child.scope is not None # type: ignore + + def test_modifier_disable(self): + ast = getASTfromString("* cube(1);") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + mod = ast[0] + assert isinstance(mod, ModularModifierDisable) + assert mod.child.scope is not None # type: ignore + + +class TestFunctionLiteralRecursion: + """Test function literal recursion support.""" + + def test_function_literal_sees_assigned_variable(self): + ast = getASTfromString("fn = function(n) n == 0 ? 1 : n * fn(n-1);") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + + assignment = ast[0] + func_lit = assignment.expr # type: ignore + assert isinstance(func_lit, FunctionLiteral) + + # The function body should see 'fn' for recursion + body = func_lit.body + assert hasattr(body, 'scope') + assert body.scope.lookup_variable("fn") is not None # type: ignore + + def test_function_literal_with_default_parameter(self): + """Function literal with default parameter visits default in caller scope.""" + ast = getASTfromString("f = function(x = 1) x;") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + assignment = ast[0] + func_lit = assignment.expr # type: ignore + assert func_lit.body.scope.lookup_variable("x") is not None # type: ignore + + def test_function_literal_in_expression(self): + """FunctionLiteral in expression (not assigned) gets scope with pending_var=None.""" + ast = getASTfromString("x = (function(a) a)(1);") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + # FunctionLiteral is inside PrimaryCall; body should see param a + assign = ast[0] + pc = assign.expr # type: ignore + fl = pc.left # type: ignore + assert fl.body.scope.lookup_variable("a") is not None # type: ignore + + def test_function_literal_in_ternary_rhs_sees_assigned_variable(self): + """Function literals in a ternary RHS should see the variable being assigned.""" + ast = getASTfromString("a = b ? function(x, n) a(x + n, n - 1) : function(x, n) a(x * n, n - 1);") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + assignment = ast[0] + ternary = assignment.expr # type: ignore + true_fl = ternary.true_expr # type: ignore + false_fl = ternary.false_expr # type: ignore + assert isinstance(true_fl, FunctionLiteral) + assert isinstance(false_fl, FunctionLiteral) + # Both function bodies should see 'a' for recursion + assert true_fl.body.scope.lookup_variable("a") is not None # type: ignore + assert false_fl.body.scope.lookup_variable("a") is not None # type: ignore + + +class TestModularCallChildren: + """Test that module call children get their own scope.""" + + def test_modular_call_with_named_argument(self): + """ModularCall with NamedArgument visits name and expr.""" + ast = getASTfromString("cube(size=1);") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + call = ast[0] + assert isinstance(call, ModularCall) + assert len(call.arguments) >= 1 + assert call.arguments[0].name.scope is not None # type: ignore + + def test_primary_call_named_argument_visits_name(self): + """PrimaryCall with NamedArgument: _visit_node visits arg.name (Identifier).""" + ast = getASTfromString("a = cube(size=1);") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + assign = ast[0] + assert hasattr(assign, "expr") + pc = assign.expr # type: ignore + assert hasattr(pc, "arguments") and len(pc.arguments) >= 1 # type: ignore + arg0 = pc.arguments[0] # type: ignore + if hasattr(arg0, "name"): # NamedArgument + assert arg0.name.scope is not None # type: ignore + + def test_modular_call_children_scope(self): + ast = getASTfromString(""" + module outer() { children(); } + outer() { + x = 10; + cube(x); + } + """) + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + + # Find the modular call + mod_call = ast[1] + assert isinstance(mod_call, ModularCall) + + # Children should have their own scope with x + if mod_call.children: + child = mod_call.children[0] + # x should be visible in children scope due to hoisting + assert hasattr(child, 'scope') + assert child.scope.lookup_variable("x") is not None # type: ignore + + +class TestScopeLookup: + """Test scope lookup through parent chain.""" + + def test_lookup_function_in_ancestor_scope(self): + """lookup_function finds function in grandparent when not in parent.""" + ast = getASTfromString("function f() = 1; module m() { g = f(); }") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + mod_decl = ast[1] + # Assignment g=f() is in module; f is in root + assign = next(c for c in mod_decl.children if isinstance(c, Assignment)) # type: ignore + assert assign.scope.lookup_function("f") is not None # type: ignore + + def test_lookup_module_in_ancestor_scope(self): + """lookup_module finds module in grandparent when not in parent.""" + ast = getASTfromString("module outer() { function inner() = 1; }") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + mod_decl = ast[0] + func_decl = next(c for c in mod_decl.children if isinstance(c, FunctionDeclaration)) # type: ignore + # inner's expr scope: function -> module -> root; outer is in root + assert func_decl.expr.scope.lookup_module("outer") is not None # type: ignore + + def test_lookup_in_parent_scope(self): + ast = getASTfromString(""" + x = 10; + function foo(a) = a + x; + """) + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + + func_decl = ast[1] + expr = func_decl.expr # type: ignore + + # x is not in function scope directly, but should be found in parent + assert expr.scope.lookup_variable("x") is not None + # a is in function scope directly + assert expr.scope.lookup_variable("a") is not None + + def test_shadowing(self): + ast = getASTfromString(""" + x = 10; + function foo(x) = x + 1; + """) + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + + func_decl = ast[1] + expr = func_decl.expr # type: ignore + + # x in function scope should be the parameter, not global + x_binding = expr.scope.lookup_variable("x") + assert x_binding is not None + # It should be the parameter, which is in the local scope + assert "x" in expr.scope.variables + + +class TestThreeNamespaces: + """Test that three namespaces are independent.""" + + def test_same_name_in_all_namespaces(self): + ast = getASTfromString(""" + thing = 10; + function thing() = 20; + module thing() { cube(1); } + """) + assert ast is not None and isinstance(ast, list) + root = build_scopes(ast) + + assert root.lookup_variable("thing") is not None + assert root.lookup_function("thing") is not None + assert root.lookup_module("thing") is not None + + # They should all be different nodes + var = root.lookup_variable("thing") + func = root.lookup_function("thing") + mod = root.lookup_module("thing") + + assert isinstance(var, Assignment) + assert isinstance(func, FunctionDeclaration) + assert isinstance(mod, ModuleDeclaration) + + +class TestListComprehensionScope: + """Test list comprehension scoping (ListCompFor, ListCompCFor, ListCompLet, ListCompIf, ListCompIfElse, ListCompEach).""" + + def test_list_comp_for_scope(self): + ast = getASTfromString("x = [for (i = [0:2]) i];") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + comp = ast[0].expr # type: ignore + assert isinstance(comp, ListComprehension) + lc_for = comp.elements[0] + assert isinstance(lc_for, ListCompFor) + assert lc_for.body.scope.lookup_variable("i") is not None # type: ignore + + def test_list_comp_c_for_scope(self): + ast = getASTfromString("x = [for (i = 0; i < 3; i = i + 1) i];") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + comp = ast[0].expr # type: ignore + assert isinstance(comp, ListComprehension) + lc_cfor = comp.elements[0] + assert isinstance(lc_cfor, ListCompCFor) + assert lc_cfor.body.scope.lookup_variable("i") is not None # type: ignore + + def test_list_comp_let_scope(self): + """ListCompLet with body that matches listcomp_elements (nested for).""" + ast = getASTfromString("x = [let(a = 1) for (i = [0:1]) a];") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + comp = ast[0].expr # type: ignore + assert isinstance(comp, ListComprehension) + lc_let = comp.elements[0] + assert isinstance(lc_let, ListCompLet) + assert lc_let.body.scope.lookup_variable("a") is not None # type: ignore + + def test_list_comp_if_scope(self): + """ListCompIf (for body) visits condition and true_expr (no new scope).""" + ast = getASTfromString("x = [for (i = [0:5]) if (i > 0) i];") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + comp = ast[0].expr # type: ignore + assert isinstance(comp, ListComprehension) + lc_for = comp.elements[0] + assert isinstance(lc_for, ListCompFor) + assert isinstance(lc_for.body, ListCompIf) + assert lc_for.body.true_expr.scope is not None # type: ignore + + def test_list_comp_if_else_scope(self): + """ListCompIfElse (for body) visits condition, true_expr, false_expr (no new scope).""" + ast = getASTfromString("x = [for (i = [0:5]) if (i > 0) i else -i];") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + comp = ast[0].expr # type: ignore + assert isinstance(comp, ListComprehension) + lc_for = comp.elements[0] + assert isinstance(lc_for, ListCompFor) + assert isinstance(lc_for.body, ListCompIfElse) + assert lc_for.body.true_expr.scope is not None # type: ignore + assert lc_for.body.false_expr.scope is not None # type: ignore + + def test_list_comp_each_scope(self): + ast = getASTfromString("x = [each [1, 2, 3]];") + assert ast is not None and isinstance(ast, list) + build_scopes(ast) + comp = ast[0].expr # type: ignore + assert isinstance(comp, ListComprehension) + lc_each = comp.elements[0] + assert isinstance(lc_each, ListCompEach) + assert lc_each.body.scope is not None # type: ignore diff --git a/uv.lock b/uv.lock index c9eb450..5d12880 100644 --- a/uv.lock +++ b/uv.lock @@ -1,12 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.7" -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", - "python_full_version == '3.8.*'", - "python_full_version < '3.8'", -] +requires-python = ">=3.11" [[package]] name = "arpeggio" @@ -26,65 +20,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] -[[package]] -name = "exceptiongroup" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, - { name = "typing-extensions", version = "4.13.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, - { name = "typing-extensions", version = "4.15.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.9' and python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, -] - -[[package]] -name = "importlib-metadata" -version = "6.7.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", version = "4.7.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, - { name = "zipp", marker = "python_full_version < '3.8'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/82/f6e29c8d5c098b6be61460371c2c5591f4a335923639edec43b3830650a4/importlib_metadata-6.7.0.tar.gz", hash = "sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4", size = 53569, upload-time = "2023-06-18T21:44:35.024Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/94/64287b38c7de4c90683630338cf28f129decbba0a44f0c6db35a873c73c4/importlib_metadata-6.7.0-py3-none-any.whl", hash = "sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5", size = 22934, upload-time = "2023-06-18T21:44:33.441Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.8'", -] -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.9.*'", - "python_full_version == '3.8.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, @@ -92,7 +31,7 @@ wheels = [ [[package]] name = "openscad-parser" -version = "0.9.2" +version = "2.0.1" source = { editable = "." } dependencies = [ { name = "arpeggio" }, @@ -100,80 +39,33 @@ dependencies = [ [package.optional-dependencies] dev = [ - { name = "pytest", version = "7.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, - { name = "pytest", version = "8.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "pytest" }, +] +yaml = [ + { name = "pyyaml" }, ] [package.metadata] requires-dist = [ { name = "arpeggio", specifier = ">=2.0.3" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, + { name = "pyyaml", marker = "extra == 'yaml'", specifier = ">=6.0" }, ] -provides-extras = ["dev"] - -[[package]] -name = "packaging" -version = "24.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.8'", -] -sdist = { url = "https://files.pythonhosted.org/packages/ee/b5/b43a27ac7472e1818c4bafd44430e69605baefe1f34440593e0332ec8b4d/packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9", size = 147882, upload-time = "2024-03-10T09:39:28.33Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", size = 53488, upload-time = "2024-03-10T09:39:25.947Z" }, -] +provides-extras = ["dev", "yaml"] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", - "python_full_version == '3.8.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] -[[package]] -name = "pluggy" -version = "1.2.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.8'", -] -dependencies = [ - { name = "importlib-metadata", marker = "python_full_version < '3.8'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8a/42/8f2833655a29c4e9cb52ee8a2be04ceac61bcff4a680fb338cbd3d1e322d/pluggy-1.2.0.tar.gz", hash = "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3", size = 61613, upload-time = "2023-06-21T09:12:28.745Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/32/4a79112b8b87b21450b066e102d6608907f4c885ed7b04c3fdb085d4d6ae/pluggy-1.2.0-py3-none-any.whl", hash = "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", size = 17695, upload-time = "2023-06-21T09:12:27.397Z" }, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.8.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, -] - [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, @@ -188,83 +80,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] -[[package]] -name = "pytest" -version = "7.4.4" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.8'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.8' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.8'" }, - { name = "importlib-metadata", marker = "python_full_version < '3.8'" }, - { name = "iniconfig", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, - { name = "packaging", version = "24.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, - { name = "pluggy", version = "1.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, - { name = "tomli", version = "2.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.8'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/80/1f/9d8e98e4133ffb16c90f3b405c43e38d3abb715bb5d7a63a5a684f7e46a3/pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280", size = 1357116, upload-time = "2023-12-31T12:00:18.035Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/ff/f6e8b8f39e08547faece4bd80f89d5a8de68a38b2d179cc1c4490ffa3286/pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8", size = 325287, upload-time = "2023-12-31T12:00:13.963Z" }, -] - -[[package]] -name = "pytest" -version = "8.3.5" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.8.*'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version == '3.8.*' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.8.*'" }, - { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, - { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, - { name = "pluggy", version = "1.5.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, - { name = "tomli", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.8.*'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, -] - -[[package]] -name = "pytest" -version = "8.4.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.9.*'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version == '3.9.*' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.9.*'" }, - { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, - { name = "pygments", marker = "python_full_version == '3.9.*'" }, - { name = "tomli", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.9.*'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, -] - [[package]] name = "pytest" version = "9.0.2" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", -] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "packaging", version = "25.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pluggy", version = "1.6.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "pygments", marker = "python_full_version >= '3.10'" }, - { name = "tomli", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ @@ -272,113 +97,56 @@ wheels = [ ] [[package]] -name = "tomli" -version = "2.0.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.8'", -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f", size = 15164, upload-time = "2022-02-08T10:54:04.006Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", size = 12757, upload-time = "2022-02-08T10:54:02.017Z" }, -] - -[[package]] -name = "tomli" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", - "python_full_version == '3.8.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" }, - { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" }, - { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" }, - { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" }, - { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" }, - { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" }, - { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" }, - { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" }, - { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" }, - { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" }, - { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" }, - { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" }, - { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" }, - { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" }, - { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" }, - { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" }, - { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" }, - { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" }, - { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" }, - { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" }, - { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" }, - { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" }, - { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" }, - { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" }, - { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" }, - { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" }, - { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" }, - { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" }, - { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" }, - { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" }, - { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" }, - { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" }, - { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" }, - { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" }, - { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" }, - { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" }, - { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" }, - { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.7.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.8'", -] -sdist = { url = "https://files.pythonhosted.org/packages/3c/8b/0111dd7d6c1478bf83baa1cab85c686426c7a6274119aceb2bd9d35395ad/typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2", size = 72876, upload-time = "2023-07-02T14:20:55.045Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/6b/63cc3df74987c36fe26157ee12e09e8f9db4de771e0f3404263117e75b95/typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", size = 33232, upload-time = "2023-07-02T14:20:53.275Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.13.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version == '3.8.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.10'", - "python_full_version == '3.9.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "zipp" -version = "3.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/00/27/f0ac6b846684cecce1ee93d32450c45ab607f65c2e0255f0092032d91f07/zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b", size = 18454, upload-time = "2023-02-25T02:17:22.503Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/fa/c9e82bbe1af6266adf08afb563905eb87cab83fde00a0a08963510621047/zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556", size = 6758, upload-time = "2023-02-25T02:17:20.807Z" }, +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ]