diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 0000000..911a072 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,84 @@ +name: Python CI + +on: + push: + pull_request: + +permissions: + contents: read + +concurrency: + group: python-ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + typing: + name: Static typing + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + cache: pip + - run: python -m pip install ".[dev]" + - run: pyright + + test: + name: Unit tests (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + - run: python -m pip install . + - run: python -m unittest discover -s tests -v + + coverage: + name: Coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + cache: pip + - run: python -m pip install ".[dev]" + - run: coverage run -m unittest discover -s tests -v + - run: coverage report + - run: coverage xml + - uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage.xml + + package: + name: Build and verify distribution + needs: [typing, test, coverage] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: "3.10" + cache: pip + - run: python -m pip install build + - run: python -m build + - run: python -m pip install --force-reinstall dist/*.whl + - name: Smoke-test the installed wheel outside the checkout + working-directory: /tmp + run: >- + python -c "from SmallPackage import SmallOS, SmallOSConfig, SmallTask, Unix; + assert SmallOS(config=SmallOSConfig()).setKernel(Unix())" + - uses: actions/upload-artifact@v4 + with: + name: python-distributions + path: dist/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..659e075 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +__pycache__/ +*.py[cod] +.coverage +coverage.xml +htmlcov/ +build/ +dist/ +*.egg-info/ + diff --git a/README.md b/README.md index 48bf207..a8ef905 100644 --- a/README.md +++ b/README.md @@ -73,17 +73,51 @@ That makes it a good fit for: Desktop development: ```bash -python3 -m venv .venv +python3.10 -m venv .venv source .venv/bin/activate -pip install -e . +python -m pip install -e ".[dev]" ``` +smallOS supports CPython 3.10 and newer. Python 3.6 through 3.9 are no +longer supported. MicroPython compatibility is maintained separately because +its language and standard-library support do not map directly to a CPython +release number; checker-only imports are kept off embedded runtime paths. + Run the test suite: ```bash python3 -m unittest discover -s tests -v ``` +Run the tests with the same branch-coverage gate used by CI: + +```bash +coverage run -m unittest discover -s tests -v +coverage report +``` + +Run the static type checker: + +```bash +pyright +``` + +Build the wheel and source distribution: + +```bash +python -m build +``` + +The GitHub Actions pipeline runs four gates: Pyright, unit tests across Python +3.10–3.13, branch coverage with a 60% floor, and distribution verification. +Packaging runs only after the earlier gates pass, installs the built wheel, +and smoke-tests it outside the source checkout. + +The package ships a `py.typed` marker. Type coverage is being tightened by +subsystem: configuration, awaitables, task lifecycle, scheduling, signals, +platform kernels, and core utilities form the current checked boundary, while +protocol clients, shells, and demos remain on the incremental typing backlog. + ## Quick Start Minimal desktop runtime: diff --git a/SmallPackage/Kernel.py b/SmallPackage/Kernel.py index 1a34fbf..d51a16d 100644 --- a/SmallPackage/Kernel.py +++ b/SmallPackage/Kernel.py @@ -14,11 +14,22 @@ shared MicroPython transport/time API. """ +from __future__ import annotations + +try: + from typing import TYPE_CHECKING +except ImportError: # pragma: no cover - exercised on constrained runtimes + TYPE_CHECKING = False + +if TYPE_CHECKING: + from collections.abc import Iterable, Mapping, Sequence + from typing import Any + _UNSET = object() -def _import_first(*module_names): +def _import_first(*module_names: str) -> Any | None: """ Import the first available module name from a list. @@ -35,7 +46,7 @@ def _import_first(*module_names): return None -def _portable_shell_split(line): +def _portable_shell_split(line: str) -> list[str]: """ Tokenize one shell command line without relying on desktop-only helpers. @@ -90,7 +101,7 @@ def _portable_shell_split(line): return tokens -def detect_micropython_machine_name(sys_mod=None, os_mod=None): +def detect_micropython_machine_name(sys_mod: Any = None, os_mod: Any = None) -> str: """ Best-effort lookup of the active board/firmware machine name. @@ -122,7 +133,9 @@ def detect_micropython_machine_name(sys_mod=None, os_mod=None): return '' -def build_micropython_kernel(machine_name=None, **kwargs): +def build_micropython_kernel( + machine_name: str | None = None, **kwargs: Any +) -> MicroPythonKernel: """ Return the best built-in MicroPython kernel profile for the current board. @@ -157,14 +170,14 @@ class Kernel: requiring protocol-specific kernel methods. ''' - def __init__(self): + def __init__(self) -> None: self._scheduler_anchor_tick = None self._scheduler_elapsed_ms = 0 - def write(self, msg): + def write(self, msg: str) -> None: pass - def shell_split(self, line): + def shell_split(self, line: str) -> list[str]: """ Tokenize one shell command line. @@ -174,22 +187,22 @@ def shell_split(self, line): """ return _portable_shell_split(line) - def time_epoch(self): - pass + def time_epoch(self) -> float: + raise NotImplementedError - def ticks_ms(self): - pass + def ticks_ms(self) -> int: + raise NotImplementedError - def ticks_add(self, base, delta_ms): + def ticks_add(self, base: int, delta_ms: int) -> int: return base + delta_ms - def ticks_diff(self, end, start): + def ticks_diff(self, end: int, start: int) -> int: return end - start - def time_monotonic(self): + def time_monotonic(self) -> float: return self.scheduler_now_ms() / 1000 - def scheduler_now_ms(self): + def scheduler_now_ms(self) -> int: ''' Returns a non-decreasing scheduler clock in milliseconds. @@ -208,17 +221,24 @@ def scheduler_now_ms(self): self._scheduler_anchor_tick = current return self._scheduler_elapsed_ms - def sleep(self, secs): + def sleep(self, secs: float) -> None: self.sleep_ms(int(max(0, secs) * 1000)) return - def sleep_ms(self, delay_ms): + def sleep_ms(self, delay_ms: int) -> None: pass - def io_wait(self, readables, writables, timeout_ms=None): + def io_wait( + self, + readables: Iterable[Any], + writables: Iterable[Any], + timeout_ms: int | None = None, + ) -> tuple[list[Any], list[Any]]: return [], [] - def validate_io_wait_object(self, obj): + def validate_io_wait_object( + self, obj: Any + ) -> tuple[bool, BaseException | None]: """ Return whether ``obj`` still looks safe to hand to poll/select. @@ -245,28 +265,28 @@ def validate_io_wait_object(self, obj): ) return True, None - def resolve_address(self, host, port): + def resolve_address(self, host: str, port: int) -> Any: return None - def socket_open(self, address_info): + def socket_open(self, address_info: Any) -> Any: return None - def socket_setblocking(self, sock, flag): + def socket_setblocking(self, sock: Any, flag: bool) -> None: return - def socket_connect(self, sock, sockaddr): + def socket_connect(self, sock: Any, sockaddr: Any) -> bool: return True - def socket_connection_error(self, sock): + def socket_connection_error(self, sock: Any) -> int: return 0 - def socket_send(self, sock, data): + def socket_send(self, sock: Any, data: bytes) -> int: return 0 - def socket_recv(self, sock, buffer_size): + def socket_recv(self, sock: Any, buffer_size: int) -> bytes: return b'' - def socket_close(self, sock): + def socket_close(self, sock: Any) -> None: return def socket_wrap_tls_client( @@ -280,10 +300,10 @@ def socket_wrap_tls_client( ): return sock - def socket_do_handshake(self, sock): + def socket_do_handshake(self, sock: Any) -> None: return - def _extract_errno(self, exc): + def _extract_errno(self, exc: BaseException) -> int | None: errno_value = getattr(exc, 'errno', None) if errno_value is not None: return errno_value @@ -291,13 +311,13 @@ def _extract_errno(self, exc): return exc.args[0] return None - def socket_needs_read(self, exc): + def socket_needs_read(self, exc: BaseException) -> bool: return isinstance(exc, BlockingIOError) - def socket_needs_write(self, exc): + def socket_needs_write(self, exc: BaseException) -> bool: return False - def _poll_lookup_key(self, obj): + def _poll_lookup_key(self, obj: Any) -> Any: """ Return the identity key used to map poll events back to registered objects. @@ -506,9 +526,15 @@ def __init__( modules = modules or {} - self._time = modules.get('time') or _import_first('time', 'utime') + time_mod = modules.get('time') or _import_first('time', 'utime') + if time_mod is None: + raise ImportError('MicroPythonKernel requires time or utime.') + self._time = time_mod self._select = modules.get('select') or _import_first('select', 'uselect') - self._socket = modules.get('socket') or _import_first('socket', 'usocket') + socket_mod = modules.get('socket') or _import_first('socket', 'usocket') + if socket_mod is None: + raise ImportError('MicroPythonKernel requires socket or usocket.') + self._socket = socket_mod self._ssl = modules.get('ssl') if self._ssl is None: self._ssl = _import_first('ssl', 'ussl') @@ -680,6 +706,8 @@ def socket_wrap_tls_client( except TypeError as exc: last_error = exc else: + if last_error is None: + raise RuntimeError('TLS wrapper did not produce a socket.') raise last_error if hasattr(wrapped, 'setblocking'): wrapped.setblocking(False) @@ -694,12 +722,14 @@ def socket_needs_read(self, exc): err = self._extract_errno(exc) if err in {11, 115}: return True - if self._ssl and hasattr(self._ssl, 'SSLWantReadError') and isinstance(exc, self._ssl.SSLWantReadError): + want_read_error = getattr(self._ssl, 'SSLWantReadError', None) + if isinstance(want_read_error, type) and isinstance(exc, want_read_error): return True return isinstance(exc, BlockingIOError) def socket_needs_write(self, exc): - if self._ssl and hasattr(self._ssl, 'SSLWantWriteError') and isinstance(exc, self._ssl.SSLWantWriteError): + want_write_error = getattr(self._ssl, 'SSLWantWriteError', None) + if isinstance(want_write_error, type) and isinstance(exc, want_write_error): return True return False diff --git a/SmallPackage/OSlist.py b/SmallPackage/OSlist.py index dc60ab5..3c96836 100644 --- a/SmallPackage/OSlist.py +++ b/SmallPackage/OSlist.py @@ -10,9 +10,21 @@ queue per priority, and sleeping tasks live in a wake-time heap. """ +from __future__ import annotations + from collections import deque import heapq +try: + from typing import TYPE_CHECKING +except ImportError: # pragma: no cover + TYPE_CHECKING = False + +if TYPE_CHECKING: + from typing import Literal + + from .SmallTask import SmallTask + from .list_util.binSearchList import insert, search from .SmallPID import SmallPID @@ -22,7 +34,7 @@ class OSList(SmallPID): Combined PID registry and queue manager for the cooperative scheduler. """ - def __init__(self, priors=5, length=2**12): + def __init__(self, priors: int = 5, length: int = 2**12) -> None: """Create the PID registry plus ready/sleep queue structures.""" SmallPID.__init__(self, length) self.num_priorities = priors @@ -37,7 +49,7 @@ def resetCatSel(self): """Compatibility no-op kept for older callers.""" return - def insert(self, task): + def insert(self, task: SmallTask) -> int: """Assign a PID and register a task in the PID-sorted backing list.""" priority = task.priority if not 0 < priority < self.num_priorities: @@ -55,7 +67,7 @@ def insert(self, task): self.tasks.insert(index, task) return pid - def search(self, pid): + def search(self, pid: int) -> SmallTask | Literal[-1]: """Look up a task by PID.""" length = len(self.tasks) index = search(self.tasks, pid, 0, length, self.func) @@ -63,7 +75,7 @@ def search(self, pid): return -1 return self.tasks[index] - def delete(self, pid): + def delete(self, pid: int) -> int: """Remove a task from PID storage and watcher accounting.""" length = len(self.tasks) index = search(self.tasks, pid, 0, length, self.func) @@ -77,7 +89,7 @@ def delete(self, pid): self.freePID(pid) return 0 - def enqueue(self, task, front=False): + def enqueue(self, task: SmallTask, front: bool = False) -> int: """ Put a runnable task on its per-priority ready queue. @@ -99,7 +111,7 @@ def enqueue(self, task, front=False): task._queued = True return 0 - def pop(self): + def pop(self) -> SmallTask | None: """ Return the next runnable task. @@ -118,12 +130,12 @@ def pop(self): return task return None - def add_sleeping(self, task, wake_time): + def add_sleeping(self, task: SmallTask, wake_time: int) -> None: """Push a sleeping task onto the wake-time heap.""" self._sleep_seq += 1 heapq.heappush(self.sleeping, (wake_time, self._sleep_seq, task)) - def wake_sleeping(self, now): + def wake_sleeping(self, now: int) -> list[SmallTask]: """ Return every task whose scheduled wake time has arrived. @@ -140,7 +152,7 @@ def wake_sleeping(self, now): ready.append(task) return ready - def next_wake_time(self): + def next_wake_time(self) -> int | None: """Peek at the next valid wake time, discarding stale heap entries.""" while self.sleeping: wake_time, _, task = self.sleeping[0] @@ -150,18 +162,18 @@ def next_wake_time(self): return wake_time return None - def list(self): + def list(self) -> list[SmallTask]: """Return a snapshot list of currently registered tasks.""" return [task for task in self.tasks] - def isOnlyWatchers(self): + def isOnlyWatchers(self) -> bool: """Report whether every remaining task is marked as a watcher.""" return len(self.tasks) == self.numWatchers - def __len__(self): + def __len__(self) -> int: """Return the number of registered tasks.""" return len(self.tasks) - def __str__(self): + def __str__(self) -> str: """Return a newline-separated dump of all known tasks.""" return "\n".join([str(x) for x in self.tasks]) diff --git a/SmallPackage/SmallConfig.py b/SmallPackage/SmallConfig.py index e94b841..f3a051a 100644 --- a/SmallPackage/SmallConfig.py +++ b/SmallPackage/SmallConfig.py @@ -7,6 +7,19 @@ points. """ +from __future__ import annotations + +try: + from typing import TYPE_CHECKING +except ImportError: # pragma: no cover - exercised on constrained runtimes + TYPE_CHECKING = False + +if TYPE_CHECKING: + from os import PathLike + from typing import Any, Mapping + + from ._types import SmallOSConfigData + _DEFAULT_CLIENT_DEFAULTS = { "stream": { @@ -64,12 +77,12 @@ class SmallOSConfig: def __init__( self, - task_capacity=2**10, - priority_levels=10, - io_buffer_length=1024, - eternal_watchers=False, - client_defaults=None, - ): + task_capacity: int = 2**10, + priority_levels: int = 10, + io_buffer_length: int = 1024, + eternal_watchers: bool = False, + client_defaults: Mapping[str, Mapping[str, int]] | None = None, + ) -> None: self.task_capacity = self._validate_positive_int("task_capacity", task_capacity) self.priority_levels = self._validate_positive_int("priority_levels", priority_levels) if self.priority_levels < 2: @@ -79,7 +92,7 @@ def __init__( self.client_defaults = self._normalize_client_defaults(client_defaults) @staticmethod - def _validate_positive_int(name, value): + def _validate_positive_int(name: str, value: int) -> int: if not isinstance(value, int): raise TypeError("{} must be an int.".format(name)) if value <= 0: @@ -87,7 +100,7 @@ def _validate_positive_int(name, value): return value @staticmethod - def _validate_non_negative_int(name, value): + def _validate_non_negative_int(name: str, value: int) -> int: if not isinstance(value, int): raise TypeError("{} must be an int.".format(name)) if value < 0: @@ -95,14 +108,16 @@ def _validate_non_negative_int(name, value): return value @classmethod - def _default_client_defaults(cls): + def _default_client_defaults(cls) -> dict[str, dict[str, int]]: defaults = {} for section, values in _DEFAULT_CLIENT_DEFAULTS.items(): defaults[section] = dict(values) return defaults @classmethod - def _normalize_client_defaults(cls, client_defaults): + def _normalize_client_defaults( + cls, client_defaults: Mapping[str, Mapping[str, int]] | None + ) -> dict[str, dict[str, int]]: if client_defaults is None: return cls._default_client_defaults() if not isinstance(client_defaults, dict): @@ -128,12 +143,14 @@ def _normalize_client_defaults(cls, client_defaults): return normalized @classmethod - def default(cls): + def default(cls) -> SmallOSConfig: """Return a fresh config populated with the runtime defaults.""" return cls() @classmethod - def from_dict(cls, data): + def from_dict( + cls, data: SmallOSConfig | Mapping[str, Any] | None + ) -> SmallOSConfig: """Build a config from canonical keys or supported aliases.""" if data is None: return cls() @@ -151,19 +168,19 @@ def from_dict(cls, data): ) @classmethod - def from_json_file(cls, path): + def from_json_file(cls, path: str | PathLike[str]) -> SmallOSConfig: """Load a config from a JSON file on desktop Python or MicroPython.""" json_mod = _import_json_module() with open(path, "r") as handle: return cls.from_dict(json_mod.load(handle)) - def copy(self, **updates): + def copy(self, **updates: Any) -> SmallOSConfig: """Return a new config with a few fields replaced.""" - data = self.to_dict() + data: dict[str, Any] = dict(self.to_dict()) data.update(updates) return type(self).from_dict(data) - def to_dict(self): + def to_dict(self) -> SmallOSConfigData: """Return a plain-serializable dictionary with canonical config keys.""" return { "task_capacity": self.task_capacity, @@ -175,7 +192,7 @@ def to_dict(self): }, } - def client_defaults_for(self, section): + def client_defaults_for(self, section: str) -> dict[str, int]: """ Return one client section merged with the shared stream-level defaults. diff --git a/SmallPackage/SmallIO.py b/SmallPackage/SmallIO.py index 883d49a..187c3f7 100644 --- a/SmallPackage/SmallIO.py +++ b/SmallPackage/SmallIO.py @@ -7,8 +7,20 @@ back to app view flushes the buffered output in order. """ +from __future__ import annotations + from collections import deque +try: + from typing import TYPE_CHECKING +except ImportError: # pragma: no cover + TYPE_CHECKING = False + +if TYPE_CHECKING: + from typing import Any + + from ._types import TerminalStatus + class SmallIO: """ @@ -22,25 +34,26 @@ class SmallIO: - flush buffered output """ - def __init__(self, buffer_length): + def __init__(self, buffer_length: int) -> None: """Set up the shell/app terminal split plus the app output buffer.""" + self.kernel: Any = None self.terminalToggle = False self.buffer_length = max(0, int(buffer_length)) self.appPrintQueue = deque(maxlen=self.buffer_length) if self.buffer_length else deque() return - def _coerce_message(self, *args): + def _coerce_message(self, *args: object) -> str: """Join arbitrary print arguments into one text payload.""" return "".join(str(arg) for arg in args) - def _write_direct(self, msg): + def _write_direct(self, msg: str) -> bool: """Write straight to the active kernel when one is attached.""" if not getattr(self, "kernel", None): return False self.kernel.write(msg) return True - def print(self, *args): + def print(self, *args: object) -> None: """ Write application output. @@ -57,7 +70,7 @@ def print(self, *args): self.appPrintQueue.append(msg) return - def sPrint(self, *args, force=False): + def sPrint(self, *args: object, force: bool = False) -> None: """ Write shell or OS output. @@ -70,7 +83,7 @@ def sPrint(self, *args, force=False): self._write_direct(msg) return - def terminalStatus(self): + def terminalStatus(self) -> TerminalStatus: """Return a small snapshot of the terminal/buffer state.""" return { "terminal_visible": bool(self.terminalToggle), @@ -78,17 +91,17 @@ def terminalStatus(self): "buffer_length": self.buffer_length, } - def getBufferedOutput(self): + def getBufferedOutput(self) -> list[str]: """Return a copy of the buffered app output.""" return list(self.appPrintQueue) - def clearBufferedOutput(self): + def clearBufferedOutput(self) -> int: """Drop every buffered app message and return how many were removed.""" removed = len(self.appPrintQueue) self.appPrintQueue.clear() return removed - def flushBufferedOutput(self): + def flushBufferedOutput(self) -> int: """Write all buffered app output immediately and return the flush count.""" flushed = 0 while self.appPrintQueue: @@ -97,7 +110,7 @@ def flushBufferedOutput(self): flushed += 1 return flushed - def setTerminalMode(self, enabled): + def setTerminalMode(self, enabled: bool) -> TerminalStatus: """Explicitly switch between shell view and app view.""" enabled = bool(enabled) if self.terminalToggle == enabled: @@ -109,6 +122,6 @@ def setTerminalMode(self, enabled): self.flushBufferedOutput() return self.terminalStatus() - def toggleTerminal(self): + def toggleTerminal(self) -> TerminalStatus: """Toggle between shell view and app view.""" return self.setTerminalMode(not self.terminalToggle) diff --git a/SmallPackage/SmallOS.py b/SmallPackage/SmallOS.py index d1713aa..9f95180 100644 --- a/SmallPackage/SmallOS.py +++ b/SmallPackage/SmallOS.py @@ -11,6 +11,21 @@ - a runtime shape that is easier to port to MicroPython """ +from __future__ import annotations + +try: + from typing import TYPE_CHECKING +except ImportError: # pragma: no cover + TYPE_CHECKING = False + +if TYPE_CHECKING: + from collections.abc import Iterable + from typing import Any + + from .Kernel import Kernel + from .SmallTask import SmallTask + from ._types import ErrorHandler, RuntimeErrorEvent, SmallOSConfigData + from .awaitables import TaskInstruction from .SmallIO import SmallIO from .SmallConfig import SmallOSConfig @@ -31,7 +46,12 @@ class SmallOS(SmallIO): and easier to customize. """ - def __init__(self, size=None, config=None, **kwargs): + def __init__( + self, + size: int | None = None, + config: SmallOSConfig | SmallOSConfigData | None = None, + **kwargs: Any, + ) -> None: """ Create the runtime shell plus its task and shell registries. @@ -79,11 +99,11 @@ def __init__(self, size=None, config=None, **kwargs): shells.setOS(self) self.shells.append(shells) - def startOS(self): + def startOS(self) -> None: """Compatibility entrypoint kept from the earlier project API.""" return self.start() - def start(self): + def start(self) -> None: """ Run the scheduler until no live tasks remain. @@ -113,12 +133,12 @@ def start(self): return return - def next(self): + def next(self) -> SmallTask | None: """Return the next runnable task without advancing the main loop.""" self.cursor = self.tasks.pop() return self.cursor - def fork(self, children): + def fork(self, children: SmallTask | list[SmallTask]) -> int | list[int]: """Register one task or a list of tasks with the runtime.""" if isinstance(children, list): ids = [] @@ -127,7 +147,7 @@ def fork(self, children): return ids return self._fork_one(children) - def _fork_one(self, task): + def _fork_one(self, task: SmallTask) -> int: """Assign a PID, attach the runtime, and enqueue the task if runnable.""" pid = self.tasks.insert(task) if pid == -1: @@ -138,17 +158,19 @@ def _fork_one(self, task): self.tasks.enqueue(task) return pid - def setKernel(self, kernel): + def setKernel(self, kernel: Kernel) -> SmallOS: """Attach the platform abstraction used for time and output.""" self.kernel = kernel return self - def setEternalWatchers(self, isEternalWatcherPresent): + def setEternalWatchers(self, isEternalWatcherPresent: bool) -> SmallOS: """Control whether the runtime exits once only watcher tasks remain.""" self.eternalWatchers = isEternalWatcherPresent return self - def setErrorHandler(self, handler, include_cancelled=False): + def setErrorHandler( + self, handler: ErrorHandler | None, include_cancelled: bool = False + ) -> SmallOS: """ Install a best-effort runtime error observer for failed tasks. @@ -196,7 +218,13 @@ def _idle_until_next_task(self): self.kernel.sleep_ms(timeout) return True - def resume_task(self, task, value=_MISSING, exc=None, front=False): + def resume_task( + self, + task: SmallTask, + value: Any = _MISSING, + exc: BaseException | None = None, + front: bool = False, + ) -> int: """ Requeue a blocked task with the value or exception that completed it. @@ -213,13 +241,13 @@ def resume_task(self, task, value=_MISSING, exc=None, front=False): self.tasks.enqueue(task, front=front) return 0 - def on_signal(self, task, sig): + def on_signal(self, task: SmallTask, sig: int) -> None: """Wake a task immediately if it is actively waiting on ``sig``.""" if task._blocked_reason == "signal" and task._waiting_signal == sig: task.signals[sig] = 0 self.resume_task(task, value=sig, front=True) - def _handle_yield(self, task, yielded): + def _handle_yield(self, task: SmallTask, yielded: Any) -> None: """ Interpret one scheduler instruction emitted by a task. @@ -320,13 +348,14 @@ def _handle_yield(self, task, yielded): task.fail(UnsupportedAwaitableError("Unknown instruction {!r}".format(operation))) self._finalize_task(task) - def _resolve_task(self, target): + def _resolve_task(self, target: int | SmallTask) -> SmallTask | None: """Normalize either a task object or a PID to a task object.""" - if hasattr(target, "getID"): + if not isinstance(target, int): return target - return self.tasks.search(target) + found = self.tasks.search(target) + return None if found == -1 else found - def _normalize_targets(self, targets): + def _normalize_targets(self, targets: Iterable[int | SmallTask]) -> list[SmallTask] | None: """Resolve a join target list while preserving caller-specified order.""" normalized = [] seen = set() @@ -423,7 +452,7 @@ def _register_io_wait(self, task, io_obj, mode): if task not in waiters[io_obj]: waiters[io_obj].append(task) - def _wake_io_tasks(self, timeout_ms=0): + def _wake_io_tasks(self, timeout_ms: int | None = 0): """ Ask the kernel which I/O objects are ready and resume their waiters. @@ -647,7 +676,7 @@ def _finalize_task(self, task): if failure_event is not None: self._dispatch_error_handler(failure_event) - def cancel_task(self, task, recursive=False): + def cancel_task(self, task: int | SmallTask, recursive: bool = False) -> int: """Cancel a task by object or PID and optionally cancel its descendants.""" target = self._resolve_task(task) if target is None or target == -1: @@ -667,7 +696,7 @@ def cancel_task(self, task, recursive=False): self._finalize_task(target) return 0 - def __str__(self): + def __str__(self) -> str: """Return a human-readable dump of the currently registered tasks.""" all_tasks = list(self.tasks.tasks) string = "SmallOS\n" diff --git a/SmallPackage/SmallPID.py b/SmallPackage/SmallPID.py index 5d28a45..b38e762 100644 --- a/SmallPackage/SmallPID.py +++ b/SmallPackage/SmallPID.py @@ -1,13 +1,16 @@ -class SmallPID(): +from __future__ import annotations + + +class SmallPID: ''' @class smallPID() - Creates and keeps track of avialable Process ID's ''' - def __init__(self, max=2**16): + def __init__(self, max: int = 2**16) -> None: ''' @function __init__() - Initializes essential maintenence structures. @@ -21,7 +24,7 @@ def __init__(self, max=2**16): return - def newPID(self): + def newPID(self) -> int: ''' @function newPID() - Creates a new valid PID. @return - int - positive integer on success @@ -40,7 +43,7 @@ def newPID(self): return -1 - def freePID(self,pid): + def freePID(self, pid: int) -> None: ''' @function freePID - removes the pid from the used pid set() making the pid available for future @@ -49,4 +52,4 @@ def freePID(self,pid): ''' if pid in self.usedPID: self.usedPID.remove(pid) - return \ No newline at end of file + return diff --git a/SmallPackage/SmallSignals.py b/SmallPackage/SmallSignals.py index 825b37a..9063cbe 100644 --- a/SmallPackage/SmallSignals.py +++ b/SmallPackage/SmallSignals.py @@ -7,6 +7,22 @@ ``SmallOS`` later interprets those awaitables and moves tasks between queues. """ +from __future__ import annotations + +try: + from typing import TYPE_CHECKING +except ImportError: # pragma: no cover - exercised on constrained runtimes + TYPE_CHECKING = False + +if TYPE_CHECKING: + from collections.abc import Callable + from typing import Any, ClassVar, cast + + from .SmallOS import SmallOS + from .SmallTask import SmallTask + from .TaskState import TaskState + from .awaitables import _InstructionAwaitable + from .awaitables import ( sleep_instruction, wait_readable_instruction, @@ -31,15 +47,19 @@ class SmallSignals: and app code should use named constants instead of bare integers. """ - SIGNAL_CAPACITY = 32 - CORE_SIGNAL_MEANINGS = {} - LEGACY_SIGNAL_MEANINGS = { + SIGNAL_CAPACITY: ClassVar[int] = 32 + CORE_SIGNAL_MEANINGS: ClassVar[dict[int, str]] = {} + LEGACY_SIGNAL_MEANINGS: ClassVar[dict[int, str]] = { 5: "Legacy waitOnAsync parent-wait signal from the pre-native-async runtime.", 6: "Legacy waitOnAsync child-complete signal from the pre-native-async runtime.", 7: "Legacy waitOnAsync all-children-complete signal from the pre-native-async runtime.", } - def __init__(self, OS, kwargs): + if TYPE_CHECKING: + OS: SmallOS | None + state: TaskState + + def __init__(self, OS: SmallOS | None, kwargs: dict[str, Any]) -> None: """ Initialize per-task signal state. @@ -52,14 +72,14 @@ def __init__(self, OS, kwargs): self.wakeSigs = [] self.sleepTime = 0 self.timeOfSleep = 0 - self.handlers = None + self.handlers: Callable[[SmallSignals], Any] | None = None super().__init__() if kwargs and kwargs.get("handlers", False): self.handlers = kwargs["handlers"] - def getSignals(self): + def getSignals(self) -> list[int]: """Return the list of currently latched signal numbers.""" received = [] for num, sig in enumerate(self.signals): @@ -68,7 +88,7 @@ def getSignals(self): return received @classmethod - def describeSignal(cls, sig): + def describeSignal(cls, sig: int) -> str: """ Return the documented meaning of a signal slot. @@ -82,7 +102,7 @@ def describeSignal(cls, sig): return cls.LEGACY_SIGNAL_MEANINGS[sig] + " Not used by the current runtime." return "Application-defined signal slot." - def sendSignal(self, pid, sig): + def sendSignal(self, pid: int, sig: int) -> int: """ Deliver a signal to another task by PID. @@ -102,7 +122,7 @@ def sendSignal(self, pid, sig): task.acceptSignal(sig) return 0 - def acceptSignal(self, sig): + def acceptSignal(self, sig: int) -> int: """ Latch an incoming signal and notify the scheduler. @@ -114,13 +134,16 @@ def acceptSignal(self, sig): self.signals[sig] = 1 if self.OS: - self.OS.on_signal(self, sig) + task = cast("SmallTask", self) if TYPE_CHECKING else self + self.OS.on_signal(task, sig) if self.handlers: self.handlers(self) return 0 - def sleep(self, secs, state_blob=None): + def sleep( + self, secs: float, state_blob: dict[Any, Any] | None = None + ) -> _InstructionAwaitable[None]: """ Return the awaitable used for cooperative sleeping. @@ -131,35 +154,40 @@ def sleep(self, secs, state_blob=None): self.state.update(state_blob) return sleep_instruction(secs) - def wait_signal(self, sig, state_blob=None): + def wait_signal( + self, sig: int, state_blob: dict[Any, Any] | None = None + ) -> _InstructionAwaitable[int]: """Return the awaitable used to wait until ``sig`` is delivered.""" if state_blob is not None: self.state.update(state_blob) return wait_signal_instruction(sig) - def sigSuspendV2(self, sig, state_blob=None): + def sigSuspendV2( + self, sig: int, state_blob: dict[Any, Any] | None = None + ) -> _InstructionAwaitable[int]: """Compatibility alias for the older generator-era suspension name.""" return self.wait_signal(sig, state_blob) - def yield_now(self): + def yield_now(self) -> _InstructionAwaitable[None]: """Return the awaitable used for an explicit cooperative yield.""" return yield_now_instruction() - def wait_readable(self, io_obj): + def wait_readable(self, io_obj: Any) -> _InstructionAwaitable[Any]: """Return the awaitable used to wait until ``io_obj`` is readable.""" return wait_readable_instruction(io_obj) - def wait_writable(self, io_obj): + def wait_writable(self, io_obj: Any) -> _InstructionAwaitable[Any]: """Return the awaitable used to wait until ``io_obj`` is writable.""" return wait_writable_instruction(io_obj) - def wake(self): + def wake(self) -> None: """Force the task back onto the ready queue if it belongs to an OS.""" if self.OS: - self.OS.resume_task(self) + task = cast("SmallTask", self) if TYPE_CHECKING else self + self.OS.resume_task(task) return - def checkSignal(self, sig): + def checkSignal(self, sig: int) -> bool: """ Consume and clear a previously received signal if it exists. diff --git a/SmallPackage/SmallTask.py b/SmallPackage/SmallTask.py index 2a88215..91a47a4 100644 --- a/SmallPackage/SmallTask.py +++ b/SmallPackage/SmallTask.py @@ -7,8 +7,24 @@ small because most task-local lifecycle details live here. """ +from __future__ import annotations + import inspect +try: + from typing import TYPE_CHECKING +except ImportError: # pragma: no cover - exercised on constrained runtimes + TYPE_CHECKING = False + +if TYPE_CHECKING: + from collections.abc import Iterable + from typing import Any, Generic, TypeVar + + from .SmallOS import SmallOS + from ._types import TaskRoutine + + T = TypeVar("T") + from .awaitables import join_instruction, join_all_instruction from .SmallErrors import PIDError, TaskCancelledError from .SmallSignals import SmallSignals @@ -19,7 +35,7 @@ _MISSING = object() -class SmallTask(SmallSignals, Node): +class SmallTask(SmallSignals, Node, Generic[T] if TYPE_CHECKING else object): """ Priority-scheduled unit of execution managed by ``SmallOS``. @@ -29,7 +45,7 @@ class SmallTask(SmallSignals, Node): to ``asyncio``. """ - def __init__(self, priority, routine, **kwargs): + def __init__(self, priority: int, routine: TaskRoutine[T], **kwargs: Any) -> None: """ Create a task shell around a routine or coroutine object. @@ -51,7 +67,7 @@ def __init__(self, priority, routine, **kwargs): self.updateFunc = None self.routine = routine - self._coroutine = None + self._coroutine: Any = None self._done = False self._result = None self._exception = None @@ -87,17 +103,17 @@ def __init__(self, priority, routine, **kwargs): self.args = kwargs["args"] @property - def done(self): + def done(self) -> bool: """Whether the task has reached a terminal state.""" return self._done @property - def result(self): + def result(self) -> T | None: """Return the stored result after successful completion.""" return self._result @property - def exception(self): + def exception(self) -> BaseException | None: """Return the stored terminal exception, if any.""" return self._exception @@ -193,7 +209,7 @@ def update(self): return 0 return -1 - def complete(self, result): + def complete(self, result: T | None) -> T | None: """Mark the task as successfully finished and store its result.""" self._done = True self._result = result @@ -203,7 +219,7 @@ def complete(self, result): self.state.update({"return_status": 0, "result": result}, "system") return result - def fail(self, exc): + def fail(self, exc: BaseException) -> BaseException: """Mark the task as failed and store its terminal exception.""" self._done = True self._exception = exc @@ -213,7 +229,7 @@ def fail(self, exc): self.state.update({"return_status": -1, "exception": exc}, "system") return exc - def cancel(self, message="Task cancelled"): + def cancel(self, message: str = "Task cancelled") -> None: """ Force the task into a cancelled terminal state. @@ -252,7 +268,7 @@ def block(self, reason): self.isSleep = 1 if reason == "sleep" else 0 self.state.update({"return_status": 1, "blocked_reason": reason}, "system") - def setID(self, pid): + def setID(self, pid: int) -> None: """Assign the PID chosen by ``SmallOS`` exactly once.""" if isinstance(pid, int): if self.pid == -1: @@ -263,11 +279,11 @@ def setID(self, pid): raise TypeError("PID must be type Int") return - def getID(self): + def getID(self) -> int: """Return the task PID.""" return self.pid - def setOS(self, OS): + def setOS(self, OS: SmallOS) -> None: """Attach the task to its owning runtime.""" self.OS = OS @@ -281,7 +297,9 @@ def build(self, priority, task, ready=1, name="", parent=None): parent=parent, ) - def spawn(self, routine, priority=None, **kwargs): + def spawn( + self, routine: TaskRoutine[Any] | SmallTask[Any], priority: int | None = None, **kwargs: Any + ) -> SmallTask[Any]: """ Create and register a child task on the same runtime. @@ -291,11 +309,12 @@ def spawn(self, routine, priority=None, **kwargs): if not self.OS: raise RuntimeError("Task must belong to an OS before it can spawn children.") - child = routine - if not isinstance(child, SmallTask): + if isinstance(routine, SmallTask): + child: SmallTask[Any] = routine + if priority is not None: + child.priority = priority + else: child = SmallTask(priority or self.priority, routine, **kwargs) - elif priority is not None: - child.priority = priority child.parent = self self.OS.fork(child) @@ -307,20 +326,20 @@ def fork(self, new_task): child = self.spawn(new_task) return child.getID() - def join(self, child): + def join(self, child: int | SmallTask[Any]): """Return the awaitable used to wait for one child task.""" return join_instruction(child) - def join_all(self, children): + def join_all(self, children: Iterable[int | SmallTask[Any]]): """Return the awaitable used to wait for several child tasks.""" return join_all_instruction(children) - def add_join_waiter(self, waiter): + def add_join_waiter(self, waiter: SmallTask[Any]) -> None: """Register a task that is currently waiting on this task.""" if waiter not in self._join_waiters: self._join_waiters.append(waiter) - def discard_join_waiter(self, waiter): + def discard_join_waiter(self, waiter: SmallTask[Any]) -> None: """Remove a waiter once it no longer depends on this task.""" while waiter in self._join_waiters: self._join_waiters.remove(waiter) @@ -333,15 +352,15 @@ def kill(self, flags=None): return -1 return self.OS.cancel_task(self, recursive="-r" in flags) - def getExeStatus(self): + def getExeStatus(self) -> bool: """Report whether the scheduler may run this task right now.""" return bool(self.isReady) and not self.isLocked and not self._done - def getDelStatus(self): + def getDelStatus(self) -> bool: """Report whether the task is terminal and removable.""" return self._done and not self.isWatcher - def stat(self): + def stat(self) -> str: """Return an expanded debug dump for shell-style inspection.""" msg = "\nisReady={}\nisWaiting={}\nisSleep={}\n".format( self.isReady, @@ -354,7 +373,7 @@ def stat(self): msg += "exception={!r}\n".format(self.exception) return str(self) + msg - def __str__(self): + def __str__(self) -> str: """Return a compact single-line summary of the task state.""" name = self.name or "Unamed Process" return ( diff --git a/SmallPackage/TaskState.py b/SmallPackage/TaskState.py index cedef44..2cf8195 100644 --- a/SmallPackage/TaskState.py +++ b/SmallPackage/TaskState.py @@ -1,25 +1,35 @@ +from __future__ import annotations + import copy +try: + from typing import TYPE_CHECKING +except ImportError: # pragma: no cover + TYPE_CHECKING = False + +if TYPE_CHECKING: + from typing import Any + ''' File Deprecated as of concurrency branch update. ''' -class TaskState(): +class TaskState: ''' @class taskState - state management class for keeping track of system stats and data of task. ''' - def __init__(self): + def __init__(self) -> None: self._state = dict() self._state['system'] = dict() self._state['data'] = dict() - def update(self,dict_blob,namespace='data'): + def update(self, dict_blob: dict[Any, Any], namespace: str = 'data') -> None: ''' @function updateState() - updates the contents of the state object. @@ -32,7 +42,7 @@ def update(self,dict_blob,namespace='data'): return - def isFree(self,key,namespace='data'): + def isFree(self, key: Any, namespace: str = 'data') -> bool: ''' @function isFree() - checks to see if the key is free for use in the state dict. @@ -50,7 +60,7 @@ def isFree(self,key,namespace='data'): return True - def free(self,key,namespace='data'): + def free(self, key: Any, namespace: str = 'data') -> int: ''' @function free() - Will free the selected key from the state. @param - key - key to be deleted. @@ -65,7 +75,7 @@ def free(self,key,namespace='data'): return -1 - def getState(self,key=None,namespace='data'): + def getState(self, key: Any = None, namespace: str = 'data') -> tuple[Any, int]: ''' @function getState - can return the entire state or just one element of the state returns a -1 if the requested key does not exist. @@ -83,6 +93,3 @@ def getState(self,key=None,namespace='data'): return copy.deepcopy(self._state[namespace][key]), 0 else: return None, -1 - - - diff --git a/SmallPackage/_types.py b/SmallPackage/_types.py new file mode 100644 index 0000000..b217170 --- /dev/null +++ b/SmallPackage/_types.py @@ -0,0 +1,76 @@ +"""Shared static types for smallOS. + +Runtime modules import these definitions only while type checking so embedded +targets do not need to provide the :mod:`typing` module. +""" + +from __future__ import annotations + +from collections.abc import Awaitable, Callable, Iterable, Mapping, Sequence +from typing import Any, Protocol, TypeAlias, TypedDict, TypeVar + + +T = TypeVar("T") + + +class TaskLike(Protocol): + """Minimum task interface used by queues and scheduler integrations.""" + + priority: int + isWatcher: bool + @property + def done(self) -> bool: ... + + def getID(self) -> int: ... + + +class KernelLike(Protocol): + """Platform operations consumed by the scheduler and output layer.""" + + def write(self, msg: str) -> Any: ... + def scheduler_now_ms(self) -> int: ... + def sleep_ms(self, delay_ms: int) -> None: ... + def io_wait( + self, + readables: Iterable[Any], + writables: Iterable[Any], + timeout_ms: int | None = None, + ) -> tuple[Sequence[Any], Sequence[Any]]: ... + + +class RuntimeErrorEvent(TypedDict): + task_id: int + task_name: str + parent_id: int | None + exception: BaseException + exception_type: type[BaseException] + exception_repr: str + is_cancelled: bool + blocked_reason: str | None + waiting_signal: int | None + io_wait_mode: str | None + join_target_id: int | None + join_pending_ids: list[int] + traceback_text: str + + +class SmallOSConfigData(TypedDict, total=False): + task_capacity: int + oslist_length: int + priority_levels: int + num_categories: int + io_buffer_length: int + eternal_watchers: bool + client_defaults: Mapping[str, Mapping[str, int]] + clients: Mapping[str, Mapping[str, int]] + + +class TerminalStatus(TypedDict): + terminal_visible: bool + buffered_messages: int + buffer_length: int + + +TaskRoutine: TypeAlias = Callable[..., T | Awaitable[T]] +ErrorHandler: TypeAlias = Callable[[RuntimeErrorEvent], None] +TaskTarget: TypeAlias = int | TaskLike diff --git a/SmallPackage/awaitables.py b/SmallPackage/awaitables.py index 5a08fe9..d3cb58e 100644 --- a/SmallPackage/awaitables.py +++ b/SmallPackage/awaitables.py @@ -11,6 +11,21 @@ - the scheduler decides when that wait is satisfied """ +from __future__ import annotations + +try: + from typing import TYPE_CHECKING +except ImportError: # pragma: no cover - exercised on constrained runtimes + TYPE_CHECKING = False + +if TYPE_CHECKING: + from collections.abc import Generator, Iterable + from typing import Any, Generic, TypeVar + + from ._types import TaskTarget + + T = TypeVar("T") + class TaskInstruction: """ @@ -21,18 +36,18 @@ class TaskInstruction: easier to debug and more realistic to port to constrained runtimes. """ - def __init__(self, operation, **payload): + def __init__(self, operation: str, **payload: Any) -> None: self.operation = operation self.payload = payload - def __repr__(self): + def __repr__(self) -> str: return "TaskInstruction(operation={!r}, payload={!r})".format( self.operation, self.payload, ) -class _InstructionAwaitable: +class _InstructionAwaitable(Generic[T] if TYPE_CHECKING else object): """ Tiny awaitable wrapper shared by the public helpers below. @@ -41,45 +56,45 @@ class _InstructionAwaitable: ``await`` expression in user code. """ - def __init__(self, instruction): + def __init__(self, instruction: TaskInstruction) -> None: self.instruction = instruction - def __await__(self): + def __await__(self) -> Generator[TaskInstruction, Any, T]: """Yield one scheduler instruction and then return the resume value.""" result = yield self.instruction return result -def sleep_instruction(seconds): +def sleep_instruction(seconds: float) -> _InstructionAwaitable[None]: """Create the awaitable used for cooperative sleeping.""" return _InstructionAwaitable(TaskInstruction("sleep", seconds=seconds)) -def wait_signal_instruction(signal): +def wait_signal_instruction(signal: int) -> _InstructionAwaitable[int]: """Create the awaitable used for waiting on a task signal.""" return _InstructionAwaitable(TaskInstruction("wait_signal", signal=signal)) -def yield_now_instruction(): +def yield_now_instruction() -> _InstructionAwaitable[None]: """Create the awaitable used for a voluntary scheduler yield.""" return _InstructionAwaitable(TaskInstruction("yield_now")) -def join_instruction(target): +def join_instruction(target: TaskTarget) -> _InstructionAwaitable[Any]: """Create the awaitable used for waiting on a single task.""" return _InstructionAwaitable(TaskInstruction("join", target=target)) -def join_all_instruction(targets): +def join_all_instruction(targets: Iterable[TaskTarget]) -> _InstructionAwaitable[list[Any]]: """Create the awaitable used for waiting on several tasks at once.""" return _InstructionAwaitable(TaskInstruction("join_all", targets=list(targets))) -def wait_readable_instruction(io_obj): +def wait_readable_instruction(io_obj: Any) -> _InstructionAwaitable[Any]: """Create the awaitable used for waiting until an I/O object is readable.""" return _InstructionAwaitable(TaskInstruction("wait_readable", io_obj=io_obj)) -def wait_writable_instruction(io_obj): +def wait_writable_instruction(io_obj: Any) -> _InstructionAwaitable[Any]: """Create the awaitable used for waiting until an I/O object is writable.""" return _InstructionAwaitable(TaskInstruction("wait_writable", io_obj=io_obj)) diff --git a/SmallPackage/list_util/binSearchList.py b/SmallPackage/list_util/binSearchList.py index 2c55f6a..8eb90f3 100644 --- a/SmallPackage/list_util/binSearchList.py +++ b/SmallPackage/list_util/binSearchList.py @@ -1,3 +1,14 @@ +from __future__ import annotations + +try: + from typing import TYPE_CHECKING +except ImportError: # pragma: no cover + TYPE_CHECKING = False + +if TYPE_CHECKING: + from collections.abc import Callable, Sequence + from typing import Any + # from numpy import log as ln # import math # import random @@ -5,7 +16,13 @@ -def search(data,target,l,r, func=None): +def search( + data: Sequence[Any], + target: Any, + l: int, + r: int, + func: Callable[[Sequence[Any], int], Any] | None = None, +) -> int: ''' @function search - Performs a binary search on a sorted list and returns the index of the found element. @@ -26,6 +43,7 @@ def search(data,target,l,r, func=None): if func == None: func = lambda dat,index: dat[index] + assert func is not None mid = ((r - l) / 2) mid = int(mid) @@ -42,7 +60,13 @@ def search(data,target,l,r, func=None): return -1 -def insert(data,target,l,r,func=None): +def insert( + data: Sequence[Any], + target: Any, + l: int, + r: int, + func: Callable[[Sequence[Any], int], Any] | None = None, +) -> int: ''' @function insert - Performs a binary search on a sorted list and returns the index of where the element should be added. @@ -63,6 +87,7 @@ def insert(data,target,l,r,func=None): if func == None: func = lambda dat,index: dat[index] + assert func is not None mid = (r - l)/2 mid = int(mid) @@ -143,4 +168,4 @@ def insert(data,target,l,r,func=None): # count = 0 # test_search() -# test_insert() \ No newline at end of file +# test_insert() diff --git a/SmallPackage/list_util/linkedList.py b/SmallPackage/list_util/linkedList.py index 16c414a..9003906 100644 --- a/SmallPackage/list_util/linkedList.py +++ b/SmallPackage/list_util/linkedList.py @@ -3,7 +3,9 @@ @file linkedList - modules to create and manipulate doubly linked list. ''' -def insertPrev(root, newNode): +from __future__ import annotations + +def insertPrev(root: Node, newNode: Node) -> None: ''' @function insertPrev() - takes in a rootNode and newNode and inserts the newNode behind the rootNode. @@ -24,7 +26,7 @@ def insertPrev(root, newNode): return -def insertNext(root,newNode): +def insertNext(root: Node, newNode: Node) -> None: ''' @function insertNext() - takes in a rootNode and newNode and inserts the newNode infront of the rootNode. @@ -45,7 +47,7 @@ def insertNext(root,newNode): return -def removeNode(node): +def removeNode(node: Node) -> None: ''' @function removeNode() - Removes the Node() from the doubly-linked-list @@ -67,7 +69,7 @@ def removeNode(node): -class Node(): +class Node: ''' @class Node() - Holds no data on its own, developed with the intent of being a super class @@ -79,13 +81,11 @@ class Node(): are not taken. *** ''' - next=None - prev=None + next: Node | None = None + prev: Node | None = None - def __init__(self): + def __init__(self) -> None: self.next = None self.prev = None return - - diff --git a/SmallPackage/py.typed b/SmallPackage/py.typed new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/SmallPackage/py.typed @@ -0,0 +1 @@ + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3df95ae --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,75 @@ +[build-system] +requires = ["setuptools>=77", "setuptools-scm>=8"] +build-backend = "setuptools.build_meta" + +[project] +name = "SmallPackage" +dynamic = ["version"] +description = "Concurrent and priority-oriented task management system" +readme = "README.md" +requires-python = ">=3.10" +license = "MIT" +license-files = ["LICENSE"] +authors = [{ name = "Michael E" }] +dependencies = [] +classifiers = [ + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Operating System :: OS Independent", + "Typing :: Typed", +] + +[project.optional-dependencies] +dev = [ + "build>=1.2", + "coverage>=7.6", + "pyright>=1.1.390", +] + +[project.urls] +Documentation = "https://github.com/MikiEEE/SmallOS" +Source = "https://github.com/MikiEEE/SmallOS" +Issues = "https://github.com/MikiEEE/SmallOS/issues" + +[tool.setuptools.packages.find] +include = ["SmallPackage*"] + +[tool.setuptools.package-data] +SmallPackage = ["py.typed"] +"SmallPackage.clients" = ["README.md"] + +[tool.setuptools_scm] +fallback_version = "0.0.0" + +[tool.pyright] +pythonVersion = "3.10" +typeCheckingMode = "basic" +include = ["SmallPackage", "demos"] +exclude = ["build", "dist"] +ignore = [ + "SmallPackage/clients", + "SmallPackage/shells.py", + "demos", +] +reportMissingTypeStubs = false +reportUnknownMemberType = false +reportUnknownArgumentType = false +reportUnknownVariableType = false +reportUnknownLambdaType = false +reportMissingImports = false + +[tool.coverage.run] +branch = true +source = ["SmallPackage"] + +[tool.coverage.report] +fail_under = 60 +show_missing = true +skip_covered = true + +[tool.coverage.xml] +output = "coverage.xml" diff --git a/setup.py b/setup.py index cf6dbd4..a1a8789 100644 --- a/setup.py +++ b/setup.py @@ -1,40 +1,9 @@ -from pathlib import Path +"""Compatibility entry point for tools that have not adopted PEP 517 yet. -from setuptools import find_packages, setup +Project metadata lives in ``pyproject.toml``. +""" +from setuptools import setup -README = Path(__file__).resolve().parent / "README.md" -try: - from setuptools_scm import get_version -except ImportError: - VERSION = "0.0.0" -else: - VERSION = get_version(root=".", fallback_version="0.0.0") - -setup( - name="SmallPackage", - version=VERSION, - packages=find_packages(), - include_package_data=True, - package_data={"SmallPackage.clients": ["README.md"]}, - install_requires=[], - author="Michael E", - description="Concurrent and Priority oriented Task Management System", - long_description=README.read_text(encoding="utf-8"), - long_description_content_type="text/markdown", - url="https://github.com/MikiEEE/SmallOS", - project_urls={ - "Documentation": "https://github.com/MikiEEE/SmallOS", - "Source": "https://github.com/MikiEEE/SmallOS", - "Issues": "https://github.com/MikiEEE/SmallOS/issues", - }, - license="MIT", - license_files=("LICENSE",), - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - ], - python_requires=">=3.6", -) +setup()