Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
python -m pip install --upgrade pip
pip install tox mdformat mdformat-gfm
- name: MDFormat check
run: mdformat --check README.md AGENTS.md SESSION_INSTRUCTIONS.md docs/tooling_evaluation.md
run: mdformat --check README.md AGENTS.md SESSION_INSTRUCTIONS.md
continue-on-error: true
- name: Lint with tox
run: tox -e lint
Expand Down
7 changes: 7 additions & 0 deletions fix_is_zero_arg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import sys

def modify_core(filepath):
with open(filepath, 'r') as f:
content = f.read()

# We will just use replace_with_git_merge_diff below.
15 changes: 14 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ build-backend = "setuptools.build_meta"
[tool.tox]
legacy_tox_ini = """
[tox]
envlist = py312, py313, lint, type
envlist = py312, py313, pyodide, micropython, lint, type

[testenv]
# install pytest in the virtualenv where commands will be executed
Expand All @@ -48,6 +48,19 @@ commands =
# NOTE: you can run any command line tool here - not just tests
pytest --cov=src --cov-append --cov-report=term-missing --cov-branch

[testenv:pyodide]
extras = test
deps =
pytest-pyodide
commands =
pytest test/ --run-in-pyodide

[testenv:micropython]
skip_install = true
allowlist_externals = micropython
commands =
micropython run_micropython_tests.py

[testenv:report]
deps = coverage
skip_install = true
Expand Down
9 changes: 9 additions & 0 deletions run_micropython_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import sys
import os

# Ensure src is in the path
sys.path.append(os.getcwd() + '/src')
sys.path.append(os.getcwd() + '/test')

import test_micropython_core
test_micropython_core.run_all_tests()
75 changes: 65 additions & 10 deletions src/closure_collector/core.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,58 @@
import inspect
from abc import ABCMeta, abstractmethod
from collections.abc import Iterable, Mapping
from itertools import chain
from pprint import pformat
from typing import Any

from closure_collector.util import ClosureCollectorException, is_rule, rebind
try:
import inspect
except ImportError: # MicroPython compatibility fallback for missing inspect
inspect = None # type: ignore[assignment]

from collections.abc import Callable
from typing import TypeVar

_FuncT = TypeVar("_FuncT", bound=Callable[..., Any])

try:
from abc import ABCMeta, abstractmethod
except ImportError: # MicroPython compatibility fallback for missing abc

class ABCMeta(type): # type: ignore[no-redef]
pass

def abstractmethod(funcobj: _FuncT) -> _FuncT: # noqa: UP047
return funcobj # type: ignore[misc]


try:
from collections.abc import Iterable, Mapping
except ImportError: # MicroPython compatibility fallback for missing collections.abc
try:
from collections.abc import Iterable, Mapping
except ImportError: # MicroPython compatibility fallback for missing collections.abc
Iterable = object # type: ignore[assignment,misc]
Mapping = object # type: ignore[assignment,misc]

try:
from itertools import chain
except ImportError: # MicroPython compatibility fallback for missing itertools

class chain: # type: ignore[no-redef]
def __init__(self, *iterables: Any):
self.iterables = iterables

def __iter__(self) -> Any:
for it in self.iterables:
yield from it

@classmethod
def from_iterable(cls, iterables: Any) -> Any:
return cls(*iterables)


try:
from pprint import pformat
except ImportError: # MicroPython compatibility fallback for missing pprint
pformat = repr # type: ignore[assignment]

from closure_collector.util import ClosureCollectorException, is_rule, is_zero_arg, rebind # noqa: E402

CLOSURE_ATTRS = {"root", "cache", "peers", "promises"}

Expand All @@ -29,6 +77,7 @@ def check(self, path):
:type path: list the path to this object, will be prepended to any errors generated
:return: list of errors that prevent items in this Aggregator from being sheared.
"""
pass

@abstractmethod
def shear(self, record_errors=False):
Expand All @@ -40,6 +89,7 @@ def shear(self, record_errors=False):

:return: a simple object representing these closures
"""
pass

@abstractmethod
def __dir__(self):
Expand All @@ -56,6 +106,7 @@ def __call__(self):

def clear_cache(self):
"""Empty any cache kept on this object"""
pass

def get_relatives(self) -> Iterable:
return ()
Expand Down Expand Up @@ -147,17 +198,21 @@ def __bool__(self):
return bool(self.promises)

def make_callable(self, value):
if callable(value) and len(inspect.signature(value).parameters) == 0:
if is_zero_arg(value):
ret = value
if isinstance(value, DynamicClosureCollector):
value.peers.add(self)
if value.root is None:
if getattr(value, "root", None) is None:
value.root = self
# if it's a closure and there is something in there
if hasattr(value, "__closure__") and value.__closure__:
for closure in value.__closure__:
if isinstance(closure.cell_contents, DynamicClosureCollector):
closure.cell_contents.peers.add(self)
try:
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this handling for different python interpreters? If so split it out into a function.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! This logic is required because MicroPython's function objects lack an __code__ attribute natively. I have extracted this logic into an is_zero_arg function within closure_collector/util.py (which is defined conditionally based on the availability of inspect). This cleans up make_callable and removes the need for repeated exception handling inline.

contents = closure.cell_contents
except AttributeError:
contents = closure
if isinstance(contents, DynamicClosureCollector):
contents.peers.add(self)
else:
ret = lambda: value
return ret
Expand Down
44 changes: 41 additions & 3 deletions src/closure_collector/util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
from numbers import Number
from types import FunctionType
try:
from numbers import Number
except ImportError: # MicroPython compatibility fallback for missing numbers
Number = (int, float, complex) # type: ignore[assignment,misc]

try:
import inspect
except ImportError: # MicroPython compatibility fallback for missing inspect
inspect = None # type: ignore[assignment]

try:
from typing import Any
except ImportError: # MicroPython compatibility fallback for missing typing
Any = object # type: ignore[assignment,misc]

try:
from types import FunctionType
except ImportError: # MicroPython compatibility fallback for missing types
FunctionType = type(lambda: None) # type: ignore[assignment,misc]


if inspect is not None:
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need a separate if? Could it be done in the try/except?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jules Does this need a separate if? Could it be done in the try/except?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent point. I have refactored this block directly into a single try: ... import inspect ... except ImportError: structure, placing both implementations of the functions tightly within their respective branches. This is much cleaner, thank you for the suggestion!


def is_zero_arg(value: Any) -> bool:
if not callable(value):
return False
return len(inspect.signature(value).parameters) == 0
else:

def is_zero_arg(value: Any) -> bool:
if not callable(value):
return False
try:
return value.__code__.co_argcount == 0
except AttributeError:
return True


class ClosureCollectorException(AttributeError):
Expand All @@ -19,7 +53,11 @@ def is_rule(func):

if getattr(func, "__closure__", False): ## TODO replace with inspect_getclosurevars, probably inspect only nonlocals
for cell in func.__closure__:
if not isinstance(cell.cell_contents, (str, Number, bytes, tuple, frozenset)):
try:
contents = cell.cell_contents
except AttributeError:
contents = cell
if not isinstance(contents, (str, Number, bytes, tuple, frozenset)):
return True
try:
if set(func.__globals__).intersection(func.__code__.co_names):
Expand Down
131 changes: 110 additions & 21 deletions src/flock/core.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,103 @@
import inspect
import warnings
from abc import ABCMeta, abstractmethod
from collections import OrderedDict, defaultdict
from collections.abc import (
Iterable,
Mapping,
MutableMapping,
MutableSequence,
Sequence,
)
from copy import copy
from itertools import chain

from closure_collector.core import CCBase, DynamicClosureCollector
from closure_collector.util import is_rule
from flock.util import FlockException
from typing import Any

try:
import inspect
except ImportError: # MicroPython compatibility fallback for missing inspect
inspect = None # type: ignore[assignment]

try:
import warnings
except ImportError: # MicroPython compatibility fallback for missing warnings

class warnings: # type: ignore[no-redef]
@staticmethod
def warn(*args: Any, **kwargs: Any) -> None:
pass


from collections.abc import Callable
from typing import TypeVar

_FuncT = TypeVar("_FuncT", bound=Callable[..., Any])

try:
from abc import ABCMeta, abstractmethod
except ImportError: # MicroPython compatibility fallback for missing abc

class ABCMeta(type): # type: ignore[no-redef]
pass

def abstractmethod(funcobj: _FuncT) -> _FuncT: # noqa: UP047
return funcobj # type: ignore[misc]


try:
from collections import OrderedDict, defaultdict
except ImportError: # MicroPython compatibility fallback for missing collections

class OrderedDict(dict): # type: ignore[no-redef]
pass

class defaultdict(dict): # type: ignore[no-redef]
def __init__(self, default_factory: Any = None, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
self.default_factory = default_factory

def __missing__(self, key: Any) -> Any:
if self.default_factory is None:
raise KeyError(key)
ret = self[key] = self.default_factory()
return ret


try:
from collections.abc import (
Iterable,
Mapping,
MutableMapping,
MutableSequence,
Sequence,
)
except ImportError:
try:
from collections.abc import Iterable, Mapping, MutableMapping, MutableSequence, Sequence
except ImportError: # MicroPython compatibility fallback for missing collections.abc
Iterable = object # type: ignore[assignment,misc]
Mapping = object # type: ignore[assignment,misc]
MutableMapping = object # type: ignore[assignment,misc]
MutableSequence = object # type: ignore[assignment,misc]
Sequence = object # type: ignore[assignment,misc]

_T = TypeVar("_T")

try:
from copy import copy
except ImportError: # MicroPython compatibility fallback for missing copy

def copy(x: _T) -> _T: # noqa: UP047
return x # type: ignore[misc]


try:
from itertools import chain
except ImportError: # MicroPython compatibility fallback for missing itertools

class chain: # type: ignore[no-redef]
def __init__(self, *iterables: Any):
self.iterables = iterables

def __iter__(self) -> Any:
for it in self.iterables:
yield from it

@classmethod
def from_iterable(cls, iterables: Any) -> Any:
return cls(*iterables)


from closure_collector.core import CCBase, DynamicClosureCollector # noqa: E402
from closure_collector.util import is_rule, is_zero_arg # noqa: E402
from flock.util import FlockException # noqa: E402

__author__ = "Andy Fundinger"

Expand All @@ -37,6 +120,7 @@ def check(self, path):
:type path: list the path to this object, will be prepended to any errors generated
:return: list of errors that prevent items in this Aggregator from being sheared.
"""
pass

@abstractmethod
def shear(self, record_errors=False) -> Iterable:
Expand Down Expand Up @@ -95,14 +179,18 @@ def __len__(self):
"Reminder to implement Mapping"

def make_callable(self, value):
if callable(value) and len(inspect.signature(value).parameters) == 0:
if is_zero_arg(value):
ret = value
# if it's a closure and there is something in there
if hasattr(value, "__closure__") and value.__closure__:
for closure in value.__closure__:
if isinstance(closure.cell_contents, DynamicClosureCollector):
closure.cell_contents.peers.add(self)
elif isinstance(value, Mapping):
try:
contents = closure.cell_contents
except AttributeError:
contents = closure
if isinstance(contents, DynamicClosureCollector):
contents.peers.add(self)
elif isinstance(value, Mapping) and Mapping is not object:
ret = FlockDict(value, root=self.root if self.root is not None else self)
else:
ret = lambda: value
Expand Down Expand Up @@ -144,6 +232,7 @@ def __getitem__(self, key):
try:
ret = promise()
except Exception as e:
print(f"Original Exception: {e}")
raise FlockException(f"Error calculating key:{key}") from e
self.cache[key] = ret
return ret
Expand Down
Loading
Loading