Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ codesorter follows `semantic versioning <https://semver.org/>`_.
Unreleased
************

**Fixed**

- Do not treat a class's own attribute as a dependency on a same-named outer definition.
An enum member or class variable named like the module constant that aliases it (for
example ``CACHE_MISS = _Sentinel.CACHE_MISS`` beside ``class _Sentinel(Enum):
CACHE_MISS = auto()``) previously forged a false ``class`` -> ``constant`` edge that
closed a cycle with the real ``constant`` -> ``class`` edge, hoisting the constant
above the class it references and raising ``NameError`` at import. A name bound in a
class's own body is now recognized as belonging to that class's namespace and imposes
no ordering on outer definitions.

********************
0.2.6 (2026/06/14)
********************
Expand Down
19 changes: 18 additions & 1 deletion codesorter/sort_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,24 @@ def _outer_scope(scope: object) -> bool:
# an annotation forging a false cycle with a real value-level edge.
if id(found["name"]) in self._lazy_annotation_names:
continue
found_name = cst.ensure_type(found["name"], cst.Name).value
name_node = cst.ensure_type(found["name"], cst.Name)
found_name = name_node.value
# A name bound at this class's own body level (an enum member or
# class variable) belongs to the class's namespace, so it must not
# forge a dependency on a same-named outer definition. Otherwise an
# enum member named like the module constant that aliases it (for
# example ``CACHE_MISS = _Sentinel.CACHE_MISS``) creates a false cycle
# that hoists the constant above the class it depends on. The scope
# must be ``node``'s own class scope; a name bound in the *enclosing*
# class (a sibling method an alias assignment references) is a real
# dependency and is kept.
name_scope = self.get_metadata(md.ScopeProvider, name_node, None)
if (
isinstance(name_scope, md.ClassScope)
and name_scope.node == node
and name_scope.assignments[found_name]
):
continue
is_import = isinstance(
next(iter(meta.assignments[found_name])),
md.ImportAssignment,
Expand Down
7 changes: 4 additions & 3 deletions tests/test_files/augmented_assignment_input.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Augmented assignments stay anchored to the constant they augment."""

from mod import extra, more
import enum
import json

ZEBRA = 1
__version__ = "1.0"
__all__ = ["Widget"]
__all__ += extra.__all__
__all__ += more.__all__
__all__ += json.__all__
__all__ += enum.__all__
APPLE = 2


Expand Down
7 changes: 4 additions & 3 deletions tests/test_files/augmented_assignment_output.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Augmented assignments stay anchored to the constant they augment."""

from mod import extra, more
import enum
import json

APPLE = 2
ZEBRA = 1
__all__ = ["Widget"]
__all__ += extra.__all__
__all__ += more.__all__
__all__ += json.__all__
__all__ += enum.__all__
__version__ = "1.0"


Expand Down
7 changes: 2 additions & 5 deletions tests/test_files/barrier_input.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
"""Definitions never cross a side-effecting statement that may depend on them."""

import sys
from pathlib import Path

ZEBRA = 1
REPO_ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(REPO_ROOT))
MIDDLE = 5
repr(MIDDLE) # a barrier: a bare statement splits the surrounding definitions

BANANA = 3
APPLE = 2
7 changes: 2 additions & 5 deletions tests/test_files/barrier_output.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
"""Definitions never cross a side-effecting statement that may depend on them."""

import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent
MIDDLE = 5
ZEBRA = 1
sys.path.insert(0, str(REPO_ROOT))
repr(MIDDLE) # a barrier: a bare statement splits the surrounding definitions

APPLE = 2
BANANA = 3
11 changes: 6 additions & 5 deletions tests/test_files/comprehension_dependency_input.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""A module-level comprehension references a function eagerly, so it depends on it."""

placeholders = {name: build(name) for name in ["a", "b"]}

def lazy():
"""A comprehension here is deferred, so it imposes no ordering."""
return [build(name) for name in ["c", "d"]]


def build(name):
"""Used eagerly by the module-level comprehension above."""
"""Used eagerly by the module-level comprehension below."""
return f"value-{name}"


def lazy():
"""A comprehension here is deferred, so it imposes no ordering."""
return [build(name) for name in ["c", "d"]]
placeholders = {name: build(name) for name in ["a", "b"]}
3 changes: 2 additions & 1 deletion tests/test_files/comprehension_dependency_output.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""A module-level comprehension references a function eagerly, so it depends on it."""


def build(name):
"""Used eagerly by the module-level comprehension above."""
"""Used eagerly by the module-level comprehension below."""
return f"value-{name}"


Expand Down
114 changes: 56 additions & 58 deletions tests/test_files/comprehensive_input.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""A comprehensive mix of constants, decorators, inheritance, and pytest fixtures."""

import os
from abc import ABC, abstractmethod
from functools import wraps
Expand All @@ -10,7 +12,34 @@
DEBUG_MODE = os.getenv("DEBUG", "false").lower() == "true"


# Functions with decorators
# Custom decorators (defined before the functions that use them)
def cache_result(func):
"""Cache the result of a function."""
cache = {}

@wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key not in cache:
cache[key] = func(*args, **kwargs)
return cache[key]

return wrapper


def validate_input(func):
"""Validate input parameters."""

@wraps(func)
def wrapper(*args, **kwargs):
for arg in args:
if arg is None:
raise ValueError("Input cannot be None")
return func(*args, **kwargs)

return wrapper


@cache_result
def expensive_calculation(n):
"""Perform an expensive calculation."""
Expand All @@ -23,24 +52,21 @@ def process_data(data):
return [item.upper() for item in data]


class Dog(Mammal):
"""Dog class."""
# Base classes with inheritance (each base defined before its subclass)
class Animal(ABC):
"""Base class for all animals."""

def __init__(self, name, age, fur_color, breed):
super().__init__(name, age, fur_color)
self.breed = breed
def __init__(self, name, age):
self.name = name
self.age = age

@abstractmethod
def make_sound(self):
"""Make a dog sound."""
return "Woof!"

def get_breed_info(self):
"""Get breed information."""
return f"Breed: {self.breed}"
"""Make a sound."""

def fetch(self):
"""Fetch behavior."""
return f"{self.name} is fetching"
def get_info(self):
"""Get animal information."""
return f"{self.name} is {self.age} years old"


class Mammal(Animal):
Expand All @@ -59,29 +85,31 @@ def get_fur_info(self):
return f"Fur color: {self.fur_color}"


# Base classes with inheritance
class Animal(ABC):
"""Base class for all animals."""
class Dog(Mammal):
"""Dog class."""

def __init__(self, name, age):
self.name = name
self.age = age
def __init__(self, name, age, fur_color, breed):
super().__init__(name, age, fur_color)
self.breed = breed

@abstractmethod
def make_sound(self):
"""Make a sound."""
"""Make a dog sound."""
return "Woof!"

def get_info(self):
"""Get animal information."""
return f"{self.name} is {self.age} years old"
def get_breed_info(self):
"""Get breed information."""
return f"Breed: {self.breed}"

def fetch(self):
"""Fetch behavior."""
return f"{self.name} is fetching"


# Classes with global dependencies
class DatabaseManager:
"""Manages database connections using global config."""

def __init__(self):
self.host = "localhost" # Would normally use DATABASE_URL
self.host = "localhost"
self.port = 5432
self.database = "test_db"

Expand Down Expand Up @@ -109,40 +137,11 @@ def is_debug_mode(self):
return DEBUG_MODE


def validate_input(func):
"""Validate input parameters."""

@wraps(func)
def wrapper(*args, **kwargs):
for arg in args:
if arg is None:
raise ValueError("Input cannot be None")
return func(*args, **kwargs)

return wrapper


def regular_function():
"""A regular function without decorators."""
return "regular"


# Custom decorators
def cache_result(func):
"""Cache the result of a function."""
cache = {}

@wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key not in cache:
cache[key] = func(*args, **kwargs)
return cache[key]

return wrapper


# Test functions
def test_something(database_connection, sample_data):
"""Test function using fixtures."""
assert database_connection["connected"]
Expand Down Expand Up @@ -171,7 +170,6 @@ def sample_data():
return ["item1", "item2", "item3"]


# Pytest fixtures
@pytest.fixture(scope="session")
def database_connection():
"""Provide a database connection for testing."""
Expand Down
14 changes: 6 additions & 8 deletions tests/test_files/comprehensive_output.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
"""A comprehensive mix of constants, decorators, inheritance, and pytest fixtures."""

import os
from abc import ABC, abstractmethod
from functools import wraps

import pytest
API_KEY = "test-key-123"

API_KEY = "test-key-123"
# Global variables
DATABASE_URL = "sqlite:///test.db"
DEBUG_MODE = os.getenv("DEBUG", "false").lower() == "true"
Expand All @@ -25,7 +27,7 @@ def make_request(self, endpoint):
return f"GET {self.base_url}{endpoint}"


# Base classes with inheritance
# Base classes with inheritance (each base defined before its subclass)
class Animal(ABC):
"""Base class for all animals."""

Expand All @@ -42,12 +44,11 @@ def get_info(self):
return f"{self.name} is {self.age} years old"


# Classes with global dependencies
class DatabaseManager:
"""Manages database connections using global config."""

def __init__(self):
self.host = "localhost" # Would normally use DATABASE_URL
self.host = "localhost"
self.port = 5432
self.database = "test_db"

Expand Down Expand Up @@ -105,7 +106,6 @@ def setup_test_environment():
API_KEY = "test-key-123"


# Pytest fixtures
@pytest.fixture(scope="session")
def database_connection():
"""Provide a database connection for testing."""
Expand All @@ -118,7 +118,7 @@ def sample_data():
return ["item1", "item2", "item3"]


# Custom decorators
# Custom decorators (defined before the functions that use them)
def cache_result(func):
"""Cache the result of a function."""
cache = {}
Expand All @@ -133,7 +133,6 @@ def wrapper(*args, **kwargs):
return wrapper


# Functions with decorators
@cache_result
def expensive_calculation(n):
"""Perform an expensive calculation."""
Expand Down Expand Up @@ -161,7 +160,6 @@ def test_global_dependencies():
assert api_client.is_debug_mode() == DEBUG_MODE


# Test functions
def test_something(database_connection, sample_data):
"""Test function using fixtures."""
assert database_connection["connected"]
Expand Down
12 changes: 12 additions & 0 deletions tests/test_files/enum_member_alias_input.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from enum import Enum, auto


def helper():
return CACHE_MISS


class _Sentinel(Enum):
CACHE_MISS = auto()


CACHE_MISS = _Sentinel.CACHE_MISS
Loading