Compare commits
No commits in common. "piker_pin_macmini" and "main" have entirely different histories.
piker_pin_
...
main
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Write(.claude/*commit_msg*)",
|
|
||||||
"Write(.claude/git_commit_msg_LATEST.md)",
|
|
||||||
"Bash(date *)",
|
|
||||||
"Bash(cp .claude/*)",
|
|
||||||
"Bash(git diff *)",
|
|
||||||
"Bash(git log *)",
|
|
||||||
"Bash(git status)"
|
|
||||||
],
|
|
||||||
"deny": [],
|
|
||||||
"ask": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
---
|
|
||||||
name: commit-msg
|
|
||||||
description: >
|
|
||||||
Generate git commit messages following project style. Use when user
|
|
||||||
wants to create a commit or asks for a commit message.
|
|
||||||
argument-hint: "[optional-scope-or-description]"
|
|
||||||
disable-model-invocation: true
|
|
||||||
allowed-tools:
|
|
||||||
- Bash(git *)
|
|
||||||
- Bash(date *)
|
|
||||||
- Bash(cp *)
|
|
||||||
- Read
|
|
||||||
- Grep
|
|
||||||
- Glob
|
|
||||||
- Write
|
|
||||||
---
|
|
||||||
|
|
||||||
When generating commit messages, always follow this process:
|
|
||||||
|
|
||||||
0. **Check for staged changes**: if `git diff --staged` is
|
|
||||||
empty, STOP and tell the user "nothing is staged!" with
|
|
||||||
a reminder to `git add` before invoking this skill.
|
|
||||||
|
|
||||||
1. **Gather context** from the staged diff and recent
|
|
||||||
history:
|
|
||||||
|
|
||||||
- Staged changes: !`git diff --staged --stat`
|
|
||||||
- Recent commits: !`git log --oneline -5`
|
|
||||||
|
|
||||||
2. **Analyze the diff**: understand what files changed and
|
|
||||||
the nature of the changes (new feature, bug fix, refactor,
|
|
||||||
etc.)
|
|
||||||
|
|
||||||
3. **Write the commit message** following these rules:
|
|
||||||
|
|
||||||
**Use the accompanying style guide:**
|
|
||||||
- See [style-guide-reference.md](style-guide-reference.md)
|
|
||||||
for detailed analysis of past commits in this repo.
|
|
||||||
|
|
||||||
**Subject Line Format:**
|
|
||||||
- Present tense verbs: Add, Drop, Fix, Use, Move, Adjust, etc.
|
|
||||||
- Target 50 chars (hard max: 67)
|
|
||||||
- Backticks around ALL code elements (classes, functions, modules, vars)
|
|
||||||
- Specific about what changed
|
|
||||||
|
|
||||||
**Body Format (optional - keep simple if warranted):**
|
|
||||||
- Max 67 char line length
|
|
||||||
- Use `-` bullets for lists
|
|
||||||
- Section markers: `Also,` `Deats,` `Other,` `Further,`
|
|
||||||
- Abbreviations: msg, bg, ctx, impl, mod, obvi, tn, fn, bc, var, prolly
|
|
||||||
- Casual yet technically precise tone
|
|
||||||
- Never write lines with only whitespace
|
|
||||||
|
|
||||||
**Common Opening Patterns:**
|
|
||||||
- New features: "Add `feature` to `module`"
|
|
||||||
- Removals: "Drop `attr` from `class`"
|
|
||||||
- Bug fixes: "Fix `issue` in `function`"
|
|
||||||
- Code moves: "Move `thing` to `new_location`"
|
|
||||||
- Adoption: "Use `new_tool` for `task`"
|
|
||||||
- Minor tweaks: "Adjust `behavior` in `component`"
|
|
||||||
|
|
||||||
4. **Write to TWO files**:
|
|
||||||
- `.claude/<timestamp>_<hash>_commit_msg.md`
|
|
||||||
* with `<timestamp>` from `date -u +%Y%m%dT%H%M%SZ` or similar
|
|
||||||
filesystem-safe format.
|
|
||||||
* and `<hash>` from `git log -1 --format=%h` first 7 chars.
|
|
||||||
- `.claude/git_commit_msg_LATEST.md` (overwrite)
|
|
||||||
|
|
||||||
5. **Always include credit footer**:
|
|
||||||
|
|
||||||
When no part of the patch was written by `claude`,
|
|
||||||
```
|
|
||||||
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
|
|
||||||
[claude-code-gh]: https://github.com/anthropics/claude-code
|
|
||||||
```
|
|
||||||
|
|
||||||
when some or all of the patch was written by `claude`
|
|
||||||
instead use,
|
|
||||||
|
|
||||||
```
|
|
||||||
(this patch was generated in some part by [`claude-code`][claude-code-gh])
|
|
||||||
[claude-code-gh]: https://github.com/anthropics/claude-code
|
|
||||||
```
|
|
||||||
|
|
||||||
Keep it concise. Match the tone of recent commits. For simple
|
|
||||||
changes, use subject line only.
|
|
||||||
|
|
@ -1,225 +0,0 @@
|
||||||
# Commit Message Style Guide for `tractor`
|
|
||||||
|
|
||||||
Analysis based on 500 recent commits from the `tractor` repository.
|
|
||||||
|
|
||||||
## Core Principles
|
|
||||||
|
|
||||||
Write commit messages that are technically precise yet casual in
|
|
||||||
tone. Use abbreviations and informal language while maintaining
|
|
||||||
clarity about what changed and why.
|
|
||||||
|
|
||||||
## Subject Line Format
|
|
||||||
|
|
||||||
### Length and Structure
|
|
||||||
- Target: ~50 chars with a hard-max of 67.
|
|
||||||
- Use backticks around code elements (72.2% of commits)
|
|
||||||
- Rarely use colons (5.2%), except for file prefixes
|
|
||||||
- End with '?' for uncertain changes (rare: 0.8%)
|
|
||||||
- End with '!' for important changes (rare: 2.0%)
|
|
||||||
|
|
||||||
### Opening Verbs (Present Tense)
|
|
||||||
|
|
||||||
Most common verbs from analysis:
|
|
||||||
- `Add` (14.4%) - wholly new features/functionality
|
|
||||||
- `Use` (4.4%) - adopt new approach/tool
|
|
||||||
- `Drop` (3.6%) - remove code/feature
|
|
||||||
- `Fix` (2.4%) - bug fixes
|
|
||||||
- `Move`/`Mv` (3.6%) - relocate code
|
|
||||||
- `Adjust` (2.0%) - minor tweaks
|
|
||||||
- `Update` (1.6%) - enhance existing feature
|
|
||||||
- `Bump` (1.2%) - dependency updates
|
|
||||||
- `Rename` (1.2%) - identifier changes
|
|
||||||
- `Set` (1.2%) - configuration changes
|
|
||||||
- `Handle` (1.0%) - add handling logic
|
|
||||||
- `Raise` (1.0%) - add error raising
|
|
||||||
- `Pass` (0.8%) - pass parameters/values
|
|
||||||
- `Support` (0.8%) - add support for something
|
|
||||||
- `Hide` (1.4%) - make private/internal
|
|
||||||
- `Always` (1.4%) - enforce consistent behavior
|
|
||||||
- `Mk` (1.4%) - make/create (abbreviated)
|
|
||||||
- `Start` (1.0%) - begin implementation
|
|
||||||
|
|
||||||
Other frequent verbs: `More`, `Change`, `Extend`, `Disable`, `Log`,
|
|
||||||
`Enable`, `Ensure`, `Expose`, `Allow`
|
|
||||||
|
|
||||||
### Backtick Usage
|
|
||||||
|
|
||||||
Always use backticks for:
|
|
||||||
- Module names: `trio`, `asyncio`, `msgspec`, `greenback`, `stackscope`
|
|
||||||
- Class names: `Context`, `Actor`, `Address`, `PldRx`, `SpawnSpec`
|
|
||||||
- Method names: `.pause_from_sync()`, `._pause()`, `.cancel()`
|
|
||||||
- Function names: `breakpoint()`, `collapse_eg()`, `open_root_actor()`
|
|
||||||
- Decorators: `@acm`, `@context`
|
|
||||||
- Exceptions: `Cancelled`, `TransportClosed`, `MsgTypeError`
|
|
||||||
- Keywords: `finally`, `None`, `False`
|
|
||||||
- Variable names: `tn`, `debug_mode`
|
|
||||||
- Complex expressions: `trio.Cancelled`, `asyncio.Task`
|
|
||||||
|
|
||||||
Most backticked terms in tractor:
|
|
||||||
`trio`, `asyncio`, `Context`, `.pause_from_sync()`, `tn`,
|
|
||||||
`._pause()`, `breakpoint()`, `collapse_eg()`, `Actor`, `@acm`,
|
|
||||||
`.cancel()`, `Cancelled`, `open_root_actor()`, `greenback`
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
Good subject lines:
|
|
||||||
```
|
|
||||||
Add `uds` to `._multiaddr`, tweak typing
|
|
||||||
Drop `DebugStatus.shield` attr, add `.req_finished`
|
|
||||||
Use `stackscope` for all actor-tree rendered "views"
|
|
||||||
Fix `.to_asyncio` inter-task-cancellation!
|
|
||||||
Bump `ruff.toml` to target py313
|
|
||||||
Mv `load_module_from_path()` to new `._code_load` submod
|
|
||||||
Always use `tuple`-cast for singleton parent addrs
|
|
||||||
```
|
|
||||||
|
|
||||||
## Body Format
|
|
||||||
|
|
||||||
### General Structure
|
|
||||||
- 43.2% of commits have no body (simple changes)
|
|
||||||
- Use blank line after subject
|
|
||||||
- Max line length: 67 chars
|
|
||||||
- Use `-` bullets for lists (28.0% of commits)
|
|
||||||
- Rarely use `*` bullets (2.4%)
|
|
||||||
|
|
||||||
### Section Markers
|
|
||||||
|
|
||||||
Use these markers to organize longer commit bodies:
|
|
||||||
- `Also,` (most common: 26 occurrences)
|
|
||||||
- `Other,` (13 occurrences)
|
|
||||||
- `Deats,` (11 occurrences) - for implementation details
|
|
||||||
- `Further,` (7 occurrences)
|
|
||||||
- `TODO,` (3 occurrences)
|
|
||||||
- `Impl details,` (2 occurrences)
|
|
||||||
- `Notes,` (1 occurrence)
|
|
||||||
|
|
||||||
### Common Abbreviations
|
|
||||||
|
|
||||||
Use these freely (sorted by frequency):
|
|
||||||
- `msg` (63) - message
|
|
||||||
- `bg` (37) - background
|
|
||||||
- `ctx` (30) - context
|
|
||||||
- `impl` (27) - implementation
|
|
||||||
- `mod` (26) - module
|
|
||||||
- `obvi` (17) - obviously
|
|
||||||
- `tn` (16) - task name
|
|
||||||
- `fn` (15) - function
|
|
||||||
- `vs` (15) - versus
|
|
||||||
- `bc` (14) - because
|
|
||||||
- `var` (14) - variable
|
|
||||||
- `prolly` (9) - probably
|
|
||||||
- `ep` (6) - entry point
|
|
||||||
- `OW` (5) - otherwise
|
|
||||||
- `rn` (4) - right now
|
|
||||||
- `sig` (4) - signal/signature
|
|
||||||
- `deps` (3) - dependencies
|
|
||||||
- `iface` (2) - interface
|
|
||||||
- `subproc` (2) - subprocess
|
|
||||||
- `tho` (2) - though
|
|
||||||
- `ofc` (2) - of course
|
|
||||||
|
|
||||||
### Tone and Style
|
|
||||||
|
|
||||||
- Casual but technical (use `XD` for humor: 23 times)
|
|
||||||
- Use `..` for trailing thoughts (108 occurrences)
|
|
||||||
- Use `Woops,` to acknowledge mistakes (4 subject lines)
|
|
||||||
- Don't be afraid to show personality while being precise
|
|
||||||
|
|
||||||
### Example Bodies
|
|
||||||
|
|
||||||
Simple with bullets:
|
|
||||||
```
|
|
||||||
Add `multiaddr` and bump up some deps
|
|
||||||
|
|
||||||
Since we're planning to use it for (discovery)
|
|
||||||
addressing, allowing replacement of the hacky (pretend)
|
|
||||||
attempt in `tractor._multiaddr` Bp
|
|
||||||
|
|
||||||
Also pin some deps,
|
|
||||||
- make us py312+
|
|
||||||
- use `pdbp` with my frame indexing fix.
|
|
||||||
- mv to latest `xonsh` for fancy cmd/suggestion injections.
|
|
||||||
|
|
||||||
Bump lock file to match obvi!
|
|
||||||
```
|
|
||||||
|
|
||||||
With section markers:
|
|
||||||
```
|
|
||||||
Use `stackscope` for all actor-tree rendered "views"
|
|
||||||
|
|
||||||
Instead of the (much more) limited and hacky `.devx._code`
|
|
||||||
impls, move to using the new `.devx._stackscope` API which
|
|
||||||
wraps the `stackscope` project.
|
|
||||||
|
|
||||||
Deats,
|
|
||||||
- make new `stackscope.extract_stack()` wrapper
|
|
||||||
- port over frame-descing to `_stackscope.pformat_stack()`
|
|
||||||
- move `PdbREPL` to use `stackscope` render approach
|
|
||||||
- update tests for new stack output format
|
|
||||||
|
|
||||||
Also,
|
|
||||||
- tweak log formatting for consistency
|
|
||||||
- add typing hints throughout
|
|
||||||
```
|
|
||||||
|
|
||||||
## Special Patterns
|
|
||||||
|
|
||||||
### WIP Commits
|
|
||||||
Rare (0.2%) - avoid committing WIP if possible
|
|
||||||
|
|
||||||
### Merge Commits
|
|
||||||
Auto-generated (4.4%), don't worry about style
|
|
||||||
|
|
||||||
### File References
|
|
||||||
- Use `module.py` or `.submodule` style
|
|
||||||
- Rarely use `file.py:line` references (0 in analysis)
|
|
||||||
|
|
||||||
### Links
|
|
||||||
- GitHub links used sparingly (3 total)
|
|
||||||
- Prefer code references over external links
|
|
||||||
|
|
||||||
## Footer
|
|
||||||
|
|
||||||
The default footer should credit `claude` (you) for helping generate
|
|
||||||
the commit msg content:
|
|
||||||
|
|
||||||
```
|
|
||||||
(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
|
|
||||||
[claude-code-gh]: https://github.com/anthropics/claude-code
|
|
||||||
```
|
|
||||||
|
|
||||||
Further, if the patch was solely or in part written
|
|
||||||
by `claude`, instead add:
|
|
||||||
|
|
||||||
```
|
|
||||||
(this patch was generated in some part by [`claude-code`][claude-code-gh])
|
|
||||||
[claude-code-gh]: https://github.com/anthropics/claude-code
|
|
||||||
```
|
|
||||||
|
|
||||||
## Summary Checklist
|
|
||||||
|
|
||||||
Before committing, verify:
|
|
||||||
- [ ] Subject line uses present tense verb
|
|
||||||
- [ ] Subject line ~50 chars (hard max 67)
|
|
||||||
- [ ] Code elements wrapped in backticks
|
|
||||||
- [ ] Body lines ≤67 chars
|
|
||||||
- [ ] Abbreviations used where natural
|
|
||||||
- [ ] Casual yet precise tone
|
|
||||||
- [ ] Section markers if body >3 paragraphs
|
|
||||||
- [ ] Technical accuracy maintained
|
|
||||||
|
|
||||||
## Analysis Metadata
|
|
||||||
|
|
||||||
```
|
|
||||||
Source: tractor repository
|
|
||||||
Commits analyzed: 500
|
|
||||||
Date range: 2019-2025
|
|
||||||
Analysis date: 2026-02-08
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
(this style guide was generated by [`claude-code`][claude-code-gh]
|
|
||||||
analyzing commit history)
|
|
||||||
|
|
||||||
[claude-code-gh]: https://github.com/anthropics/claude-code
|
|
||||||
|
|
@ -74,22 +74,16 @@ jobs:
|
||||||
# run: mypy tractor/ --ignore-missing-imports --show-traceback
|
# run: mypy tractor/ --ignore-missing-imports --show-traceback
|
||||||
|
|
||||||
|
|
||||||
testing:
|
testing-linux:
|
||||||
name: '${{ matrix.os }} Python${{ matrix.python-version }} - spawn_backend=${{ matrix.spawn_backend }}'
|
name: '${{ matrix.os }} Python ${{ matrix.python }} - ${{ matrix.spawn_backend }}'
|
||||||
timeout-minutes: 16
|
timeout-minutes: 10
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [
|
os: [ubuntu-latest]
|
||||||
ubuntu-latest,
|
python-version: ['3.13']
|
||||||
macos-latest,
|
|
||||||
]
|
|
||||||
python-version: [
|
|
||||||
'3.13',
|
|
||||||
# '3.14',
|
|
||||||
]
|
|
||||||
spawn_backend: [
|
spawn_backend: [
|
||||||
'trio',
|
'trio',
|
||||||
# 'mp_spawn',
|
# 'mp_spawn',
|
||||||
|
|
@ -97,6 +91,7 @@ jobs:
|
||||||
]
|
]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: 'Install uv + py-${{ matrix.python-version }}'
|
- name: 'Install uv + py-${{ matrix.python-version }}'
|
||||||
|
|
|
||||||
|
|
@ -102,30 +102,3 @@ venv.bak/
|
||||||
|
|
||||||
# mypy
|
# mypy
|
||||||
.mypy_cache/
|
.mypy_cache/
|
||||||
|
|
||||||
# all files under
|
|
||||||
.git/
|
|
||||||
|
|
||||||
# any commit-msg gen tmp files
|
|
||||||
.claude/*_commit_*.md
|
|
||||||
.claude/*_commit*.toml
|
|
||||||
.claude/*_commit*.txt
|
|
||||||
|
|
||||||
# nix develop --profile .nixdev
|
|
||||||
.nixdev*
|
|
||||||
|
|
||||||
# :Obsession .
|
|
||||||
Session.vim
|
|
||||||
# `gish` local `.md`-files
|
|
||||||
# TODO? better all around automation!
|
|
||||||
# -[ ] it'd be handy to also commit and sync with wtv git service?
|
|
||||||
# -[ ] everything should be put under a `.gish/` no?
|
|
||||||
gitea/
|
|
||||||
gh/
|
|
||||||
|
|
||||||
# ------ macOS ------
|
|
||||||
# Finder metadata
|
|
||||||
**/.DS_Store
|
|
||||||
|
|
||||||
# LLM conversations that should remain private
|
|
||||||
docs/conversations/
|
|
||||||
|
|
|
||||||
|
|
@ -641,15 +641,13 @@ Help us push toward the future of distributed `Python`.
|
||||||
- Typed capability-based (dialog) protocols ( see `#196
|
- Typed capability-based (dialog) protocols ( see `#196
|
||||||
<https://github.com/goodboy/tractor/issues/196>`_ with draft work
|
<https://github.com/goodboy/tractor/issues/196>`_ with draft work
|
||||||
started in `#311 <https://github.com/goodboy/tractor/pull/311>`_)
|
started in `#311 <https://github.com/goodboy/tractor/pull/311>`_)
|
||||||
- **macOS is now officially supported** and tested in CI
|
- We **recently disabled CI-testing on windows** and need help getting
|
||||||
alongside Linux!
|
it running again! (see `#327
|
||||||
- We **recently disabled CI-testing on windows** and need
|
<https://github.com/goodboy/tractor/pull/327>`_). **We do have windows
|
||||||
help getting it running again! (see `#327
|
support** (and have for quite a while) but since no active hacker
|
||||||
<https://github.com/goodboy/tractor/pull/327>`_). **We do
|
exists in the user-base to help test on that OS, for now we're not
|
||||||
have windows support** (and have for quite a while) but
|
actively maintaining testing due to the added hassle and general
|
||||||
since no active hacker exists in the user-base to help
|
latency..
|
||||||
test on that OS, for now we're not actively maintaining
|
|
||||||
testing due to the added hassle and general latency..
|
|
||||||
|
|
||||||
|
|
||||||
Feel like saying hi?
|
Feel like saying hi?
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ from tractor import (
|
||||||
MsgStream,
|
MsgStream,
|
||||||
_testing,
|
_testing,
|
||||||
trionics,
|
trionics,
|
||||||
TransportClosed,
|
|
||||||
)
|
)
|
||||||
import trio
|
import trio
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -209,15 +208,11 @@ async def main(
|
||||||
# TODO: is this needed or no?
|
# TODO: is this needed or no?
|
||||||
raise
|
raise
|
||||||
|
|
||||||
except (
|
except trio.ClosedResourceError:
|
||||||
trio.ClosedResourceError,
|
|
||||||
TransportClosed,
|
|
||||||
) as _tpt_err:
|
|
||||||
# NOTE: don't send if we already broke the
|
# NOTE: don't send if we already broke the
|
||||||
# connection to avoid raising a closed-error
|
# connection to avoid raising a closed-error
|
||||||
# such that we drop through to the ctl-c
|
# such that we drop through to the ctl-c
|
||||||
# mashing by user.
|
# mashing by user.
|
||||||
with trio.CancelScope(shield=True):
|
|
||||||
await trio.sleep(0.01)
|
await trio.sleep(0.01)
|
||||||
|
|
||||||
# timeout: int = 1
|
# timeout: int = 1
|
||||||
|
|
@ -252,7 +247,6 @@ async def main(
|
||||||
await stream.send(i)
|
await stream.send(i)
|
||||||
pytest.fail('stream not closed?')
|
pytest.fail('stream not closed?')
|
||||||
except (
|
except (
|
||||||
TransportClosed,
|
|
||||||
trio.ClosedResourceError,
|
trio.ClosedResourceError,
|
||||||
trio.EndOfChannel,
|
trio.EndOfChannel,
|
||||||
) as send_err:
|
) as send_err:
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,15 @@ async def aio_sleep_forever():
|
||||||
|
|
||||||
|
|
||||||
async def bp_then_error(
|
async def bp_then_error(
|
||||||
chan: to_asyncio.LinkedTaskChannel,
|
to_trio: trio.MemorySendChannel,
|
||||||
|
from_trio: asyncio.Queue,
|
||||||
|
|
||||||
raise_after_bp: bool = True,
|
raise_after_bp: bool = True,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
# sync with `trio`-side (caller) task
|
# sync with `trio`-side (caller) task
|
||||||
chan.started_nowait('start')
|
to_trio.send_nowait('start')
|
||||||
|
|
||||||
# NOTE: what happens here inside the hook needs some refinement..
|
# NOTE: what happens here inside the hook needs some refinement..
|
||||||
# => seems like it's still `.debug._set_trace()` but
|
# => seems like it's still `.debug._set_trace()` but
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ Verify we can dump a `stackscope` tree on a hang.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
import os
|
import os
|
||||||
import platform
|
|
||||||
import signal
|
import signal
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
|
|
@ -32,26 +31,13 @@ async def main(
|
||||||
from_test: bool = False,
|
from_test: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
if platform.system() != 'Darwin':
|
|
||||||
tpt = 'uds'
|
|
||||||
else:
|
|
||||||
# XXX, precisely we can't use pytest's tmp-path generation
|
|
||||||
# for tests.. apparently because:
|
|
||||||
#
|
|
||||||
# > The OSError: AF_UNIX path too long in macOS Python occurs
|
|
||||||
# > because the path to the Unix domain socket exceeds the
|
|
||||||
# > operating system's maximum path length limit (around 104
|
|
||||||
#
|
|
||||||
# WHICH IS just, wtf hillarious XD
|
|
||||||
tpt = 'tcp'
|
|
||||||
|
|
||||||
async with (
|
async with (
|
||||||
tractor.open_nursery(
|
tractor.open_nursery(
|
||||||
debug_mode=True,
|
debug_mode=True,
|
||||||
enable_stack_on_sig=True,
|
enable_stack_on_sig=True,
|
||||||
# maybe_enable_greenback=False,
|
# maybe_enable_greenback=False,
|
||||||
loglevel='devx',
|
loglevel='devx',
|
||||||
enable_transports=[tpt],
|
enable_transports=['uds'],
|
||||||
) as an,
|
) as an,
|
||||||
):
|
):
|
||||||
ptl: tractor.Portal = await an.start_actor(
|
ptl: tractor.Portal = await an.start_actor(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import platform
|
|
||||||
|
|
||||||
import tractor
|
import tractor
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
|
|
@ -36,22 +34,9 @@ async def just_bp(
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
|
||||||
if platform.system() != 'Darwin':
|
|
||||||
tpt = 'uds'
|
|
||||||
else:
|
|
||||||
# XXX, precisely we can't use pytest's tmp-path generation
|
|
||||||
# for tests.. apparently because:
|
|
||||||
#
|
|
||||||
# > The OSError: AF_UNIX path too long in macOS Python occurs
|
|
||||||
# > because the path to the Unix domain socket exceeds the
|
|
||||||
# > operating system's maximum path length limit (around 104
|
|
||||||
#
|
|
||||||
# WHICH IS just, wtf hillarious XD
|
|
||||||
tpt = 'tcp'
|
|
||||||
|
|
||||||
async with tractor.open_nursery(
|
async with tractor.open_nursery(
|
||||||
debug_mode=True,
|
debug_mode=True,
|
||||||
enable_transports=[tpt],
|
enable_transports=['uds'],
|
||||||
loglevel='devx',
|
loglevel='devx',
|
||||||
) as n:
|
) as n:
|
||||||
p = await n.start_actor(
|
p = await n.start_actor(
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ async def main() -> list[int]:
|
||||||
# yes, a nursery which spawns `trio`-"actors" B)
|
# yes, a nursery which spawns `trio`-"actors" B)
|
||||||
an: ActorNursery
|
an: ActorNursery
|
||||||
async with tractor.open_nursery(
|
async with tractor.open_nursery(
|
||||||
loglevel='error',
|
loglevel='cancel',
|
||||||
# debug_mode=True,
|
# debug_mode=True,
|
||||||
) as an:
|
) as an:
|
||||||
|
|
||||||
|
|
@ -118,10 +118,8 @@ async def main() -> list[int]:
|
||||||
cancelled: bool = await portal.cancel_actor()
|
cancelled: bool = await portal.cancel_actor()
|
||||||
assert cancelled
|
assert cancelled
|
||||||
|
|
||||||
print(
|
print(f"STREAM TIME = {time.time() - start}")
|
||||||
f"STREAM TIME = {time.time() - start}\n"
|
print(f"STREAM + SPAWN TIME = {time.time() - pre_start}")
|
||||||
f"STREAM + SPAWN TIME = {time.time() - pre_start}\n"
|
|
||||||
)
|
|
||||||
assert result_stream == list(range(seed))
|
assert result_stream == list(range(seed))
|
||||||
return result_stream
|
return result_stream
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,17 +11,21 @@ import tractor
|
||||||
|
|
||||||
|
|
||||||
async def aio_echo_server(
|
async def aio_echo_server(
|
||||||
chan: tractor.to_asyncio.LinkedTaskChannel,
|
to_trio: trio.MemorySendChannel,
|
||||||
|
from_trio: asyncio.Queue,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
# a first message must be sent **from** this ``asyncio``
|
# a first message must be sent **from** this ``asyncio``
|
||||||
# task or the ``trio`` side will never unblock from
|
# task or the ``trio`` side will never unblock from
|
||||||
# ``tractor.to_asyncio.open_channel_from():``
|
# ``tractor.to_asyncio.open_channel_from():``
|
||||||
chan.started_nowait('start')
|
to_trio.send_nowait('start')
|
||||||
|
|
||||||
|
# XXX: this uses an ``from_trio: asyncio.Queue`` currently but we
|
||||||
|
# should probably offer something better.
|
||||||
while True:
|
while True:
|
||||||
# echo the msg back
|
# echo the msg back
|
||||||
chan.send_nowait(await chan.get())
|
to_trio.send_nowait(await from_trio.get())
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
27
flake.lock
27
flake.lock
|
|
@ -1,27 +0,0 @@
|
||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1769018530,
|
|
||||||
"narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=",
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "88d3861acdd3d2f0e361767018218e51810df8a1",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nixos",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
70
flake.nix
70
flake.nix
|
|
@ -1,70 +0,0 @@
|
||||||
# An "impure" template thx to `pyproject.nix`,
|
|
||||||
# https://pyproject-nix.github.io/pyproject.nix/templates.html#impure
|
|
||||||
# https://github.com/pyproject-nix/pyproject.nix/blob/master/templates/impure/flake.nix
|
|
||||||
{
|
|
||||||
description = "An impure overlay (w dev-shell) using `uv`";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs =
|
|
||||||
{ nixpkgs, ... }:
|
|
||||||
let
|
|
||||||
inherit (nixpkgs) lib;
|
|
||||||
forAllSystems = lib.genAttrs lib.systems.flakeExposed;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
devShells = forAllSystems (
|
|
||||||
system:
|
|
||||||
let
|
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
|
||||||
|
|
||||||
# XXX NOTE XXX, for now we overlay specific pkgs via
|
|
||||||
# a major-version-pinned-`cpython`
|
|
||||||
cpython = "python313";
|
|
||||||
venv_dir = "py313";
|
|
||||||
pypkgs = pkgs."${cpython}Packages";
|
|
||||||
in
|
|
||||||
{
|
|
||||||
default = pkgs.mkShell {
|
|
||||||
|
|
||||||
packages = [
|
|
||||||
# XXX, ensure sh completions activate!
|
|
||||||
pkgs.bashInteractive
|
|
||||||
pkgs.bash-completion
|
|
||||||
|
|
||||||
# XXX, on nix(os), use pkgs version to avoid
|
|
||||||
# build/sys-sh-integration issues
|
|
||||||
pkgs.ruff
|
|
||||||
|
|
||||||
pkgs.uv
|
|
||||||
pkgs.${cpython}# ?TODO^ how to set from `cpython` above?
|
|
||||||
];
|
|
||||||
|
|
||||||
shellHook = ''
|
|
||||||
# unmask to debug **this** dev-shell-hook
|
|
||||||
# set -e
|
|
||||||
|
|
||||||
# link-in c++ stdlib for various AOT-ext-pkgs (numpy, etc.)
|
|
||||||
LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib}/lib:$LD_LIBRARY_PATH"
|
|
||||||
|
|
||||||
export LD_LIBRARY_PATH
|
|
||||||
|
|
||||||
# RUNTIME-SETTINGS
|
|
||||||
# ------ uv ------
|
|
||||||
# - always use the ./py313/ venv-subdir
|
|
||||||
# - sync env with all extras
|
|
||||||
export UV_PROJECT_ENVIRONMENT=${venv_dir}
|
|
||||||
uv sync --dev --all-extras
|
|
||||||
|
|
||||||
# ------ TIPS ------
|
|
||||||
# NOTE, to launch the py-venv installed `xonsh` (like @goodboy)
|
|
||||||
# run the `nix develop` cmd with,
|
|
||||||
# >> nix develop -c uv run xonsh
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -9,7 +9,7 @@ name = "tractor"
|
||||||
version = "0.1.0a6dev0"
|
version = "0.1.0a6dev0"
|
||||||
description = 'structured concurrent `trio`-"actors"'
|
description = 'structured concurrent `trio`-"actors"'
|
||||||
authors = [{ name = "Tyler Goodlet", email = "goodboy_foss@protonmail.com" }]
|
authors = [{ name = "Tyler Goodlet", email = "goodboy_foss@protonmail.com" }]
|
||||||
requires-python = ">=3.12, <3.14"
|
requires-python = ">= 3.11"
|
||||||
readme = "docs/README.rst"
|
readme = "docs/README.rst"
|
||||||
license = "AGPL-3.0-or-later"
|
license = "AGPL-3.0-or-later"
|
||||||
keywords = [
|
keywords = [
|
||||||
|
|
@ -24,13 +24,11 @@ keywords = [
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 3 - Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
"Operating System :: POSIX :: Linux",
|
"Operating System :: POSIX :: Linux",
|
||||||
"Operating System :: MacOS",
|
|
||||||
"Framework :: Trio",
|
"Framework :: Trio",
|
||||||
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
|
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)",
|
||||||
"Programming Language :: Python :: Implementation :: CPython",
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
"Programming Language :: Python :: 3.12",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.13",
|
|
||||||
"Topic :: System :: Distributed Computing",
|
"Topic :: System :: Distributed Computing",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|
@ -44,64 +42,48 @@ dependencies = [
|
||||||
"wrapt>=1.16.0,<2",
|
"wrapt>=1.16.0,<2",
|
||||||
"colorlog>=6.8.2,<7",
|
"colorlog>=6.8.2,<7",
|
||||||
# built-in multi-actor `pdb` REPL
|
# built-in multi-actor `pdb` REPL
|
||||||
"pdbp>=1.8.2,<2", # windows only (from `pdbp`)
|
"pdbp>=1.6,<2", # windows only (from `pdbp`)
|
||||||
# typed IPC msging
|
# typed IPC msging
|
||||||
"msgspec>=0.19.0",
|
"msgspec>=0.19.0",
|
||||||
"cffi>=1.17.1",
|
"cffi>=1.17.1",
|
||||||
"bidict>=0.23.1",
|
"bidict>=0.23.1",
|
||||||
"platformdirs>=4.4.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# ------ project ------
|
# ------ project ------
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
{include-group = 'devx'},
|
|
||||||
{include-group = 'testing'},
|
|
||||||
{include-group = 'repl'},
|
|
||||||
]
|
|
||||||
devx = [
|
|
||||||
# `tractor.devx` tooling
|
|
||||||
"greenback>=1.2.1,<2",
|
|
||||||
"stackscope>=0.2.2,<0.3",
|
|
||||||
# ^ requires this?
|
|
||||||
"typing-extensions>=4.14.1",
|
|
||||||
]
|
|
||||||
testing = [
|
|
||||||
# test suite
|
# test suite
|
||||||
# TODO: maybe some of these layout choices?
|
# TODO: maybe some of these layout choices?
|
||||||
# https://docs.pytest.org/en/8.0.x/explanation/goodpractices.html#choosing-a-test-layout-import-rules
|
# https://docs.pytest.org/en/8.0.x/explanation/goodpractices.html#choosing-a-test-layout-import-rules
|
||||||
"pytest>=8.3.5",
|
"pytest>=8.3.5",
|
||||||
"pexpect>=4.9.0,<5",
|
"pexpect>=4.9.0,<5",
|
||||||
]
|
# `tractor.devx` tooling
|
||||||
repl = [
|
"greenback>=1.2.1,<2",
|
||||||
|
"stackscope>=0.2.2,<0.3",
|
||||||
|
# ^ requires this?
|
||||||
|
"typing-extensions>=4.14.1",
|
||||||
|
|
||||||
"pyperclip>=1.9.0",
|
"pyperclip>=1.9.0",
|
||||||
"prompt-toolkit>=3.0.50",
|
"prompt-toolkit>=3.0.50",
|
||||||
"xonsh>=0.22.2",
|
"xonsh>=0.19.2",
|
||||||
"psutil>=7.0.0",
|
"psutil>=7.0.0",
|
||||||
]
|
]
|
||||||
lint = [
|
|
||||||
"ruff>=0.9.6"
|
|
||||||
]
|
|
||||||
# TODO, add these with sane versions; were originally in
|
# TODO, add these with sane versions; were originally in
|
||||||
# `requirements-docs.txt`..
|
# `requirements-docs.txt`..
|
||||||
# docs = [
|
# docs = [
|
||||||
# "sphinx>="
|
# "sphinx>="
|
||||||
# "sphinx_book_theme>="
|
# "sphinx_book_theme>="
|
||||||
# ]
|
# ]
|
||||||
|
|
||||||
# ------ dependency-groups ------
|
# ------ dependency-groups ------
|
||||||
|
|
||||||
|
# ------ dependency-groups ------
|
||||||
|
|
||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
# XXX NOTE, only for @goodboy's hacking on `pprint(sort_dicts=False)`
|
# XXX NOTE, only for @goodboy's hacking on `pprint(sort_dicts=False)`
|
||||||
# for the `pp` alias..
|
# for the `pp` alias..
|
||||||
|
# pdbp = { path = "../pdbp", editable = true }
|
||||||
# [tool.uv.sources.pdbp]
|
|
||||||
# XXX, in case we need to tmp patch again.
|
|
||||||
# git = "https://github.com/goodboy/pdbp.git"
|
|
||||||
# branch ="repair_stack_trace_frame_indexing"
|
|
||||||
# path = "../pdbp"
|
|
||||||
# editable = true
|
|
||||||
|
|
||||||
# ------ tool.uv.sources ------
|
# ------ tool.uv.sources ------
|
||||||
# TODO, distributed (multi-host) extensions
|
# TODO, distributed (multi-host) extensions
|
||||||
|
|
@ -163,7 +145,6 @@ all_bullets = true
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
minversion = '6.0'
|
minversion = '6.0'
|
||||||
# https://docs.pytest.org/en/stable/reference/reference.html#configuration-options
|
|
||||||
testpaths = [
|
testpaths = [
|
||||||
'tests'
|
'tests'
|
||||||
]
|
]
|
||||||
|
|
@ -174,17 +155,10 @@ addopts = [
|
||||||
'--import-mode=importlib',
|
'--import-mode=importlib',
|
||||||
# don't show frickin captured logs AGAIN in the report..
|
# don't show frickin captured logs AGAIN in the report..
|
||||||
'--show-capture=no',
|
'--show-capture=no',
|
||||||
|
|
||||||
# disable `xonsh` plugin
|
|
||||||
# https://docs.pytest.org/en/stable/how-to/plugins.html#disabling-plugins-from-autoloading
|
|
||||||
# https://docs.pytest.org/en/stable/how-to/plugins.html#deactivating-unregistering-a-plugin-by-name
|
|
||||||
'-p no:xonsh'
|
|
||||||
]
|
]
|
||||||
log_cli = false
|
log_cli = false
|
||||||
# TODO: maybe some of these layout choices?
|
# TODO: maybe some of these layout choices?
|
||||||
# https://docs.pytest.org/en/8.0.x/explanation/goodpractices.html#choosing-a-test-layout-import-rules
|
# https://docs.pytest.org/en/8.0.x/explanation/goodpractices.html#choosing-a-test-layout-import-rules
|
||||||
# pythonpath = "src"
|
# pythonpath = "src"
|
||||||
|
|
||||||
# https://docs.pytest.org/en/stable/reference/reference.html#confval-console_output_style
|
|
||||||
console_output_style = 'progress'
|
|
||||||
# ------ tool.pytest ------
|
# ------ tool.pytest ------
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
# vim: ft=ini
|
||||||
|
# pytest.ini for tractor
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
# don't show frickin captured logs AGAIN in the report..
|
||||||
|
addopts = --show-capture='no'
|
||||||
|
log_cli = false
|
||||||
|
; minversion = 6.0
|
||||||
|
|
@ -35,8 +35,8 @@ exclude = [
|
||||||
line-length = 88
|
line-length = 88
|
||||||
indent-width = 4
|
indent-width = 4
|
||||||
|
|
||||||
# assume latest minor cpython
|
# Assume Python 3.9
|
||||||
target-version = "py313"
|
target-version = "py311"
|
||||||
|
|
||||||
[lint]
|
[lint]
|
||||||
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import platform
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import tractor
|
|
||||||
from tractor._testing import (
|
from tractor._testing import (
|
||||||
examples_dir as examples_dir,
|
examples_dir as examples_dir,
|
||||||
tractor_test as tractor_test,
|
tractor_test as tractor_test,
|
||||||
|
|
@ -23,7 +22,6 @@ pytest_plugins: list[str] = [
|
||||||
'tractor._testing.pytest',
|
'tractor._testing.pytest',
|
||||||
]
|
]
|
||||||
|
|
||||||
_non_linux: bool = platform.system() != 'Linux'
|
|
||||||
|
|
||||||
# Sending signal.SIGINT on subprocess fails on windows. Use CTRL_* alternatives
|
# Sending signal.SIGINT on subprocess fails on windows. Use CTRL_* alternatives
|
||||||
if platform.system() == 'Windows':
|
if platform.system() == 'Windows':
|
||||||
|
|
@ -46,10 +44,6 @@ no_windows = pytest.mark.skipif(
|
||||||
platform.system() == "Windows",
|
platform.system() == "Windows",
|
||||||
reason="Test is unsupported on windows",
|
reason="Test is unsupported on windows",
|
||||||
)
|
)
|
||||||
no_macos = pytest.mark.skipif(
|
|
||||||
platform.system() == "Darwin",
|
|
||||||
reason="Test is unsupported on MacOS",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(
|
def pytest_addoption(
|
||||||
|
|
@ -67,54 +61,15 @@ def pytest_addoption(
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session', autouse=True)
|
@pytest.fixture(scope='session', autouse=True)
|
||||||
def loglevel(request) -> str:
|
def loglevel(request):
|
||||||
import tractor
|
import tractor
|
||||||
orig = tractor.log._default_loglevel
|
orig = tractor.log._default_loglevel
|
||||||
level = tractor.log._default_loglevel = request.config.option.loglevel
|
level = tractor.log._default_loglevel = request.config.option.loglevel
|
||||||
log = tractor.log.get_console_log(
|
tractor.log.get_console_log(level)
|
||||||
level=level,
|
|
||||||
name='tractor', # <- enable root logger
|
|
||||||
)
|
|
||||||
log.info(
|
|
||||||
f'Test-harness set runtime loglevel: {level!r}\n'
|
|
||||||
)
|
|
||||||
yield level
|
yield level
|
||||||
tractor.log._default_loglevel = orig
|
tractor.log._default_loglevel = orig
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function')
|
|
||||||
def test_log(
|
|
||||||
request,
|
|
||||||
loglevel: str,
|
|
||||||
) -> tractor.log.StackLevelAdapter:
|
|
||||||
'''
|
|
||||||
Deliver a per test-module-fn logger instance for reporting from
|
|
||||||
within actual test bodies/fixtures.
|
|
||||||
|
|
||||||
For example this can be handy to report certain error cases from
|
|
||||||
exception handlers using `test_log.exception()`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
modname: str = request.function.__module__
|
|
||||||
log = tractor.log.get_logger(
|
|
||||||
name=modname, # <- enable root logger
|
|
||||||
# pkg_name='tests',
|
|
||||||
)
|
|
||||||
_log = tractor.log.get_console_log(
|
|
||||||
level=loglevel,
|
|
||||||
logger=log,
|
|
||||||
name=modname,
|
|
||||||
# pkg_name='tests',
|
|
||||||
)
|
|
||||||
_log.debug(
|
|
||||||
f'In-test-logging requested\n'
|
|
||||||
f'test_log.name: {log.name!r}\n'
|
|
||||||
f'level: {loglevel!r}\n'
|
|
||||||
|
|
||||||
)
|
|
||||||
yield _log
|
|
||||||
|
|
||||||
|
|
||||||
_ci_env: bool = os.environ.get('CI', False)
|
_ci_env: bool = os.environ.get('CI', False)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -151,7 +106,6 @@ def daemon(
|
||||||
testdir: pytest.Pytester,
|
testdir: pytest.Pytester,
|
||||||
reg_addr: tuple[str, int],
|
reg_addr: tuple[str, int],
|
||||||
tpt_proto: str,
|
tpt_proto: str,
|
||||||
ci_env: bool,
|
|
||||||
|
|
||||||
) -> subprocess.Popen:
|
) -> subprocess.Popen:
|
||||||
'''
|
'''
|
||||||
|
|
@ -189,25 +143,13 @@ def daemon(
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO! we should poll for the registry socket-bind to take place
|
|
||||||
# and only once that's done yield to the requester!
|
|
||||||
# -[ ] TCP: use the `._root.open_root_actor()`::`ping_tpt_socket()`
|
|
||||||
# closure!
|
|
||||||
# -[ ] UDS: can we do something similar for 'pinging" the
|
|
||||||
# file-socket?
|
|
||||||
#
|
|
||||||
global _PROC_SPAWN_WAIT
|
|
||||||
# UDS sockets are **really** fast to bind()/listen()/connect()
|
# UDS sockets are **really** fast to bind()/listen()/connect()
|
||||||
# so it's often required that we delay a bit more starting
|
# so it's often required that we delay a bit more starting
|
||||||
# the first actor-tree..
|
# the first actor-tree..
|
||||||
if tpt_proto == 'uds':
|
if tpt_proto == 'uds':
|
||||||
|
global _PROC_SPAWN_WAIT
|
||||||
_PROC_SPAWN_WAIT = 0.6
|
_PROC_SPAWN_WAIT = 0.6
|
||||||
|
|
||||||
if _non_linux and ci_env:
|
|
||||||
_PROC_SPAWN_WAIT += 1
|
|
||||||
|
|
||||||
# XXX, allow time for the sub-py-proc to boot up.
|
|
||||||
# !TODO, see ping-polling ideas above!
|
|
||||||
time.sleep(_PROC_SPAWN_WAIT)
|
time.sleep(_PROC_SPAWN_WAIT)
|
||||||
|
|
||||||
assert not proc.returncode
|
assert not proc.returncode
|
||||||
|
|
@ -217,30 +159,18 @@ def daemon(
|
||||||
# XXX! yeah.. just be reaaal careful with this bc sometimes it
|
# XXX! yeah.. just be reaaal careful with this bc sometimes it
|
||||||
# can lock up on the `_io.BufferedReader` and hang..
|
# can lock up on the `_io.BufferedReader` and hang..
|
||||||
stderr: str = proc.stderr.read().decode()
|
stderr: str = proc.stderr.read().decode()
|
||||||
stdout: str = proc.stdout.read().decode()
|
if stderr:
|
||||||
if (
|
|
||||||
stderr
|
|
||||||
or
|
|
||||||
stdout
|
|
||||||
):
|
|
||||||
print(
|
print(
|
||||||
f'Daemon actor tree produced output:\n'
|
f'Daemon actor tree produced STDERR:\n'
|
||||||
f'{proc.args}\n'
|
f'{proc.args}\n'
|
||||||
f'\n'
|
f'\n'
|
||||||
f'stderr: {stderr!r}\n'
|
f'{stderr}\n'
|
||||||
f'stdout: {stdout!r}\n'
|
|
||||||
)
|
)
|
||||||
|
if proc.returncode != -2:
|
||||||
if (rc := proc.returncode) != -2:
|
raise RuntimeError(
|
||||||
msg: str = (
|
'Daemon actor tree failed !?\n'
|
||||||
f'Daemon actor tree was not cancelled !?\n'
|
f'{proc.args}\n'
|
||||||
f'proc.args: {proc.args!r}\n'
|
|
||||||
f'proc.returncode: {rc!r}\n'
|
|
||||||
)
|
)
|
||||||
if rc < 0:
|
|
||||||
raise RuntimeError(msg)
|
|
||||||
|
|
||||||
log.error(msg)
|
|
||||||
|
|
||||||
|
|
||||||
# @pytest.fixture(autouse=True)
|
# @pytest.fixture(autouse=True)
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,6 @@
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import platform
|
|
||||||
import signal
|
|
||||||
import time
|
import time
|
||||||
from typing import (
|
from typing import (
|
||||||
Callable,
|
Callable,
|
||||||
|
|
@ -34,23 +32,9 @@ if TYPE_CHECKING:
|
||||||
from pexpect import pty_spawn
|
from pexpect import pty_spawn
|
||||||
|
|
||||||
|
|
||||||
_non_linux: bool = platform.system() != 'Linux'
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_configure(config):
|
|
||||||
# register custom marks to avoid warnings see,
|
|
||||||
# https://docs.pytest.org/en/stable/how-to/writing_plugins.html#registering-custom-markers
|
|
||||||
config.addinivalue_line(
|
|
||||||
'markers',
|
|
||||||
'ctlcs_bish: test will (likely) not behave under SIGINT..'
|
|
||||||
)
|
|
||||||
|
|
||||||
# a fn that sub-instantiates a `pexpect.spawn()`
|
# a fn that sub-instantiates a `pexpect.spawn()`
|
||||||
# and returns it.
|
# and returns it.
|
||||||
type PexpectSpawner = Callable[
|
type PexpectSpawner = Callable[[str], pty_spawn.spawn]
|
||||||
[str],
|
|
||||||
pty_spawn.spawn,
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|
@ -80,61 +64,26 @@ def spawn(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
import os
|
import os
|
||||||
# disable colored tbs
|
|
||||||
os.environ['PYTHON_COLORS'] = '0'
|
os.environ['PYTHON_COLORS'] = '0'
|
||||||
# disable all ANSI color output
|
|
||||||
# os.environ['NO_COLOR'] = '1'
|
|
||||||
|
|
||||||
spawned: PexpectSpawner|None = None
|
|
||||||
|
|
||||||
def _spawn(
|
def _spawn(
|
||||||
cmd: str,
|
cmd: str,
|
||||||
**mkcmd_kwargs,
|
**mkcmd_kwargs,
|
||||||
) -> pty_spawn.spawn:
|
) -> pty_spawn.spawn:
|
||||||
nonlocal spawned
|
|
||||||
unset_colors()
|
unset_colors()
|
||||||
spawned = testdir.spawn(
|
return testdir.spawn(
|
||||||
cmd=mk_cmd(
|
cmd=mk_cmd(
|
||||||
cmd,
|
cmd,
|
||||||
**mkcmd_kwargs,
|
**mkcmd_kwargs,
|
||||||
),
|
),
|
||||||
expect_timeout=(
|
expect_timeout=3,
|
||||||
10 if _non_linux and _ci_env
|
|
||||||
else 3
|
|
||||||
),
|
|
||||||
# preexec_fn=unset_colors,
|
# preexec_fn=unset_colors,
|
||||||
# ^TODO? get `pytest` core to expose underlying
|
# ^TODO? get `pytest` core to expose underlying
|
||||||
# `pexpect.spawn()` stuff?
|
# `pexpect.spawn()` stuff?
|
||||||
)
|
)
|
||||||
return spawned
|
|
||||||
|
|
||||||
# such that test-dep can pass input script name.
|
# such that test-dep can pass input script name.
|
||||||
yield _spawn # the `PexpectSpawner`, type alias.
|
return _spawn # the `PexpectSpawner`, type alias.
|
||||||
|
|
||||||
if (
|
|
||||||
spawned
|
|
||||||
and
|
|
||||||
(ptyproc := spawned.ptyproc)
|
|
||||||
):
|
|
||||||
start: float = time.time()
|
|
||||||
timeout: float = 5
|
|
||||||
while (
|
|
||||||
ptyproc.isalive()
|
|
||||||
and
|
|
||||||
(
|
|
||||||
(_time_took := (time.time() - start))
|
|
||||||
<
|
|
||||||
timeout
|
|
||||||
)
|
|
||||||
):
|
|
||||||
ptyproc.kill(signal.SIGINT)
|
|
||||||
time.sleep(0.01)
|
|
||||||
|
|
||||||
if ptyproc.isalive():
|
|
||||||
ptyproc.kill(signal.SIGKILL)
|
|
||||||
|
|
||||||
# TODO? ensure we've cleaned up any UDS-paths?
|
|
||||||
# breakpoint()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(
|
@pytest.fixture(
|
||||||
|
|
@ -160,13 +109,7 @@ def ctlc(
|
||||||
'https://github.com/goodboy/tractor/issues/320'
|
'https://github.com/goodboy/tractor/issues/320'
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if mark.name == 'ctlcs_bish':
|
||||||
mark.name == 'ctlcs_bish'
|
|
||||||
and
|
|
||||||
use_ctlc
|
|
||||||
and
|
|
||||||
all(mark.args)
|
|
||||||
):
|
|
||||||
pytest.skip(
|
pytest.skip(
|
||||||
f'Test {node} prolly uses something from the stdlib (namely `asyncio`..)\n'
|
f'Test {node} prolly uses something from the stdlib (namely `asyncio`..)\n'
|
||||||
f'The test and/or underlying example script can *sometimes* run fine '
|
f'The test and/or underlying example script can *sometimes* run fine '
|
||||||
|
|
@ -271,13 +214,12 @@ def assert_before(
|
||||||
err_on_false=True,
|
err_on_false=True,
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
return str(child.before.decode())
|
|
||||||
|
|
||||||
|
|
||||||
def do_ctlc(
|
def do_ctlc(
|
||||||
child,
|
child,
|
||||||
count: int = 3,
|
count: int = 3,
|
||||||
delay: float|None = None,
|
delay: float = 0.1,
|
||||||
patt: str|None = None,
|
patt: str|None = None,
|
||||||
|
|
||||||
# expect repl UX to reprint the prompt after every
|
# expect repl UX to reprint the prompt after every
|
||||||
|
|
@ -289,7 +231,6 @@ def do_ctlc(
|
||||||
) -> str|None:
|
) -> str|None:
|
||||||
|
|
||||||
before: str|None = None
|
before: str|None = None
|
||||||
delay = delay or 0.1
|
|
||||||
|
|
||||||
# make sure ctl-c sends don't do anything but repeat output
|
# make sure ctl-c sends don't do anything but repeat output
|
||||||
for _ in range(count):
|
for _ in range(count):
|
||||||
|
|
@ -300,10 +241,7 @@ def do_ctlc(
|
||||||
# if you run this test manually it works just fine..
|
# if you run this test manually it works just fine..
|
||||||
if expect_prompt:
|
if expect_prompt:
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
child.expect(
|
child.expect(PROMPT)
|
||||||
PROMPT,
|
|
||||||
timeout=(child.timeout * 2) if _ci_env else child.timeout,
|
|
||||||
)
|
|
||||||
before = str(child.before.decode())
|
before = str(child.before.decode())
|
||||||
time.sleep(delay)
|
time.sleep(delay)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,6 @@ from .conftest import (
|
||||||
in_prompt_msg,
|
in_prompt_msg,
|
||||||
assert_before,
|
assert_before,
|
||||||
)
|
)
|
||||||
from ..conftest import (
|
|
||||||
_ci_env,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..conftest import PexpectSpawner
|
from ..conftest import PexpectSpawner
|
||||||
|
|
@ -54,14 +51,13 @@ if TYPE_CHECKING:
|
||||||
# - recurrent root errors
|
# - recurrent root errors
|
||||||
|
|
||||||
|
|
||||||
_non_linux: bool = platform.system() != 'Linux'
|
|
||||||
|
|
||||||
if platform.system() == 'Windows':
|
if platform.system() == 'Windows':
|
||||||
pytest.skip(
|
pytest.skip(
|
||||||
'Debugger tests have no windows support (yet)',
|
'Debugger tests have no windows support (yet)',
|
||||||
allow_module_level=True,
|
allow_module_level=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# TODO: was trying to this xfail style but some weird bug i see in CI
|
# TODO: was trying to this xfail style but some weird bug i see in CI
|
||||||
# that's happening at collect time.. pretty soon gonna dump actions i'm
|
# that's happening at collect time.. pretty soon gonna dump actions i'm
|
||||||
# thinkin...
|
# thinkin...
|
||||||
|
|
@ -197,11 +193,6 @@ def test_root_actor_bp_forever(
|
||||||
child.expect(EOF)
|
child.expect(EOF)
|
||||||
|
|
||||||
|
|
||||||
# skip on non-Linux CI
|
|
||||||
@pytest.mark.ctlcs_bish(
|
|
||||||
_non_linux,
|
|
||||||
_ci_env,
|
|
||||||
)
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'do_next',
|
'do_next',
|
||||||
(True, False),
|
(True, False),
|
||||||
|
|
@ -267,11 +258,6 @@ def test_subactor_error(
|
||||||
child.expect(EOF)
|
child.expect(EOF)
|
||||||
|
|
||||||
|
|
||||||
# skip on non-Linux CI
|
|
||||||
@pytest.mark.ctlcs_bish(
|
|
||||||
_non_linux,
|
|
||||||
_ci_env,
|
|
||||||
)
|
|
||||||
def test_subactor_breakpoint(
|
def test_subactor_breakpoint(
|
||||||
spawn,
|
spawn,
|
||||||
ctlc: bool,
|
ctlc: bool,
|
||||||
|
|
@ -494,24 +480,8 @@ def test_multi_daemon_subactors(
|
||||||
stream.
|
stream.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
non_linux = _non_linux
|
|
||||||
if non_linux and ctlc:
|
|
||||||
pytest.skip(
|
|
||||||
'Ctl-c + MacOS is too unreliable/racy for this test..\n'
|
|
||||||
)
|
|
||||||
# !TODO, if someone with more patience then i wants to muck
|
|
||||||
# with the timings on this please feel free to see all the
|
|
||||||
# `non_linux` branching logic i added on my first attempt
|
|
||||||
# below!
|
|
||||||
#
|
|
||||||
# my conclusion was that if i were to run the script
|
|
||||||
# manually, and thus as slowly as a human would, the test
|
|
||||||
# would and should pass as described in this test fn, however
|
|
||||||
# after fighting with it for >= 1hr. i decided more then
|
|
||||||
# likely the more extensive `linux` testing should cover most
|
|
||||||
# regressions.
|
|
||||||
|
|
||||||
child = spawn('multi_daemon_subactors')
|
child = spawn('multi_daemon_subactors')
|
||||||
|
|
||||||
child.expect(PROMPT)
|
child.expect(PROMPT)
|
||||||
|
|
||||||
# there can be a race for which subactor will acquire
|
# there can be a race for which subactor will acquire
|
||||||
|
|
@ -541,19 +511,8 @@ def test_multi_daemon_subactors(
|
||||||
else:
|
else:
|
||||||
raise ValueError('Neither log msg was found !?')
|
raise ValueError('Neither log msg was found !?')
|
||||||
|
|
||||||
non_linux_delay: float = 0.3
|
|
||||||
if ctlc:
|
if ctlc:
|
||||||
do_ctlc(
|
do_ctlc(child)
|
||||||
child,
|
|
||||||
delay=(
|
|
||||||
non_linux_delay
|
|
||||||
if non_linux
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if non_linux:
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# NOTE: previously since we did not have clobber prevention
|
# NOTE: previously since we did not have clobber prevention
|
||||||
# in the root actor this final resume could result in the debugger
|
# in the root actor this final resume could result in the debugger
|
||||||
|
|
@ -584,66 +543,33 @@ def test_multi_daemon_subactors(
|
||||||
# assert "in use by child ('bp_forever'," in before
|
# assert "in use by child ('bp_forever'," in before
|
||||||
|
|
||||||
if ctlc:
|
if ctlc:
|
||||||
do_ctlc(
|
do_ctlc(child)
|
||||||
child,
|
|
||||||
delay=(
|
|
||||||
non_linux_delay
|
|
||||||
if non_linux
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if non_linux:
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# expect another breakpoint actor entry
|
# expect another breakpoint actor entry
|
||||||
child.sendline('c')
|
child.sendline('c')
|
||||||
child.expect(PROMPT)
|
child.expect(PROMPT)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
before: str = assert_before(
|
assert_before(
|
||||||
child,
|
child,
|
||||||
bp_forev_parts,
|
bp_forev_parts,
|
||||||
)
|
)
|
||||||
except AssertionError:
|
except AssertionError:
|
||||||
before: str = assert_before(
|
assert_before(
|
||||||
child,
|
child,
|
||||||
name_error_parts,
|
name_error_parts,
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if ctlc:
|
if ctlc:
|
||||||
before: str = do_ctlc(
|
do_ctlc(child)
|
||||||
child,
|
|
||||||
delay=(
|
|
||||||
non_linux_delay
|
|
||||||
if non_linux
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
if non_linux:
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# should crash with the 2nd name error (simulates
|
# should crash with the 2nd name error (simulates
|
||||||
# a retry) and then the root eventually (boxed) errors
|
# a retry) and then the root eventually (boxed) errors
|
||||||
# after 1 or more further bp actor entries.
|
# after 1 or more further bp actor entries.
|
||||||
|
|
||||||
child.sendline('c')
|
child.sendline('c')
|
||||||
try:
|
child.expect(PROMPT)
|
||||||
child.expect(
|
|
||||||
PROMPT,
|
|
||||||
timeout=3,
|
|
||||||
)
|
|
||||||
except EOF:
|
|
||||||
before: str = child.before.decode()
|
|
||||||
print(
|
|
||||||
f'\n'
|
|
||||||
f'??? NEVER RXED `pdb` PROMPT ???\n'
|
|
||||||
f'\n'
|
|
||||||
f'{before}\n'
|
|
||||||
)
|
|
||||||
raise
|
|
||||||
|
|
||||||
assert_before(
|
assert_before(
|
||||||
child,
|
child,
|
||||||
name_error_parts,
|
name_error_parts,
|
||||||
|
|
@ -763,8 +689,7 @@ def test_multi_subactors_root_errors(
|
||||||
|
|
||||||
@has_nested_actors
|
@has_nested_actors
|
||||||
def test_multi_nested_subactors_error_through_nurseries(
|
def test_multi_nested_subactors_error_through_nurseries(
|
||||||
ci_env: bool,
|
spawn,
|
||||||
spawn: PexpectSpawner,
|
|
||||||
|
|
||||||
# TODO: address debugger issue for nested tree:
|
# TODO: address debugger issue for nested tree:
|
||||||
# https://github.com/goodboy/tractor/issues/320
|
# https://github.com/goodboy/tractor/issues/320
|
||||||
|
|
@ -787,16 +712,7 @@ def test_multi_nested_subactors_error_through_nurseries(
|
||||||
|
|
||||||
for send_char in itertools.cycle(['c', 'q']):
|
for send_char in itertools.cycle(['c', 'q']):
|
||||||
try:
|
try:
|
||||||
child.expect(
|
child.expect(PROMPT)
|
||||||
PROMPT,
|
|
||||||
timeout=(
|
|
||||||
6 if (
|
|
||||||
_non_linux
|
|
||||||
and
|
|
||||||
ci_env
|
|
||||||
) else -1
|
|
||||||
),
|
|
||||||
)
|
|
||||||
child.sendline(send_char)
|
child.sendline(send_char)
|
||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
|
@ -973,11 +889,6 @@ def test_different_debug_mode_per_actor(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# skip on non-Linux CI
|
|
||||||
@pytest.mark.ctlcs_bish(
|
|
||||||
_non_linux,
|
|
||||||
_ci_env,
|
|
||||||
)
|
|
||||||
def test_post_mortem_api(
|
def test_post_mortem_api(
|
||||||
spawn,
|
spawn,
|
||||||
ctlc: bool,
|
ctlc: bool,
|
||||||
|
|
@ -1222,21 +1133,12 @@ def test_ctxep_pauses_n_maybe_ipc_breaks(
|
||||||
# closed so verify we see error reporting as well as
|
# closed so verify we see error reporting as well as
|
||||||
# a failed crash-REPL request msg and can CTL-c our way
|
# a failed crash-REPL request msg and can CTL-c our way
|
||||||
# out.
|
# out.
|
||||||
|
|
||||||
# ?TODO, match depending on `tpt_proto(s)`?
|
|
||||||
# - [ ] how can we pass it into the script tho?
|
|
||||||
tpt: str = 'UDS'
|
|
||||||
if _non_linux:
|
|
||||||
tpt: str = 'TCP'
|
|
||||||
|
|
||||||
assert_before(
|
assert_before(
|
||||||
child,
|
child,
|
||||||
['peer IPC channel closed abruptly?',
|
['peer IPC channel closed abruptly?',
|
||||||
'another task closed this fd',
|
'another task closed this fd',
|
||||||
'Debug lock request was CANCELLED?',
|
'Debug lock request was CANCELLED?',
|
||||||
f"'Msgpack{tpt}Stream' was already closed locally?",
|
"TransportClosed: 'MsgpackUDSStream' was already closed locally ?",]
|
||||||
f"TransportClosed: 'Msgpack{tpt}Stream' was already closed 'by peer'?",
|
|
||||||
]
|
|
||||||
|
|
||||||
# XXX races on whether these show/hit?
|
# XXX races on whether these show/hit?
|
||||||
# 'Failed to REPl via `_pause()` You called `tractor.pause()` from an already cancelled scope!',
|
# 'Failed to REPl via `_pause()` You called `tractor.pause()` from an already cancelled scope!',
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,6 @@ from .conftest import (
|
||||||
PROMPT,
|
PROMPT,
|
||||||
_pause_msg,
|
_pause_msg,
|
||||||
)
|
)
|
||||||
from ..conftest import (
|
|
||||||
no_macos,
|
|
||||||
)
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pexpect.exceptions import (
|
from pexpect.exceptions import (
|
||||||
|
|
@ -45,7 +42,6 @@ if TYPE_CHECKING:
|
||||||
from ..conftest import PexpectSpawner
|
from ..conftest import PexpectSpawner
|
||||||
|
|
||||||
|
|
||||||
@no_macos
|
|
||||||
def test_shield_pause(
|
def test_shield_pause(
|
||||||
spawn: PexpectSpawner,
|
spawn: PexpectSpawner,
|
||||||
):
|
):
|
||||||
|
|
@ -61,7 +57,6 @@ def test_shield_pause(
|
||||||
expect(
|
expect(
|
||||||
child,
|
child,
|
||||||
'Yo my child hanging..?',
|
'Yo my child hanging..?',
|
||||||
timeout=3,
|
|
||||||
)
|
)
|
||||||
assert_before(
|
assert_before(
|
||||||
child,
|
child,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
'''
|
|
||||||
`tractor.msg.*` sub-sys test suite.
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
'''
|
|
||||||
`tractor.msg.*` test sub-pkg conf.
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
@ -1,240 +0,0 @@
|
||||||
'''
|
|
||||||
Unit tests for `tractor.msg.pretty_struct`
|
|
||||||
private-field filtering in `pformat()`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from tractor.msg.pretty_struct import (
|
|
||||||
Struct,
|
|
||||||
pformat,
|
|
||||||
iter_struct_ppfmt_lines,
|
|
||||||
)
|
|
||||||
from tractor.msg._codec import (
|
|
||||||
MsgDec,
|
|
||||||
mk_dec,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ------ test struct definitions ------ #
|
|
||||||
|
|
||||||
class PublicOnly(Struct):
|
|
||||||
'''
|
|
||||||
All-public fields for baseline testing.
|
|
||||||
|
|
||||||
'''
|
|
||||||
name: str = 'alice'
|
|
||||||
age: int = 30
|
|
||||||
|
|
||||||
|
|
||||||
class PrivateOnly(Struct):
|
|
||||||
'''
|
|
||||||
Only underscore-prefixed (private) fields.
|
|
||||||
|
|
||||||
'''
|
|
||||||
_secret: str = 'hidden'
|
|
||||||
_internal: int = 99
|
|
||||||
|
|
||||||
|
|
||||||
class MixedFields(Struct):
|
|
||||||
'''
|
|
||||||
Mix of public and private fields.
|
|
||||||
|
|
||||||
'''
|
|
||||||
name: str = 'bob'
|
|
||||||
_hidden: int = 42
|
|
||||||
value: float = 3.14
|
|
||||||
_meta: str = 'internal'
|
|
||||||
|
|
||||||
|
|
||||||
class Inner(
|
|
||||||
Struct,
|
|
||||||
frozen=True,
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Frozen inner struct with a private field,
|
|
||||||
for nesting tests.
|
|
||||||
|
|
||||||
'''
|
|
||||||
x: int = 1
|
|
||||||
_secret: str = 'nope'
|
|
||||||
|
|
||||||
|
|
||||||
class Outer(Struct):
|
|
||||||
'''
|
|
||||||
Outer struct nesting an `Inner`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
label: str = 'outer'
|
|
||||||
inner: Inner = Inner()
|
|
||||||
|
|
||||||
|
|
||||||
class EmptyStruct(Struct):
|
|
||||||
'''
|
|
||||||
Struct with zero fields.
|
|
||||||
|
|
||||||
'''
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# ------ tests ------ #
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
'struct_and_expected',
|
|
||||||
[
|
|
||||||
(
|
|
||||||
PublicOnly(),
|
|
||||||
{
|
|
||||||
'shown': ['name', 'age'],
|
|
||||||
'hidden': [],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
MixedFields(),
|
|
||||||
{
|
|
||||||
'shown': ['name', 'value'],
|
|
||||||
'hidden': ['_hidden', '_meta'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
(
|
|
||||||
PrivateOnly(),
|
|
||||||
{
|
|
||||||
'shown': [],
|
|
||||||
'hidden': ['_secret', '_internal'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
ids=[
|
|
||||||
'all-public',
|
|
||||||
'mixed-pub-priv',
|
|
||||||
'all-private',
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_field_visibility_in_pformat(
|
|
||||||
struct_and_expected: tuple[
|
|
||||||
Struct,
|
|
||||||
dict[str, list[str]],
|
|
||||||
],
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Verify `pformat()` shows public fields
|
|
||||||
and hides `_`-prefixed private fields.
|
|
||||||
|
|
||||||
'''
|
|
||||||
(
|
|
||||||
struct,
|
|
||||||
expected,
|
|
||||||
) = struct_and_expected
|
|
||||||
output: str = pformat(struct)
|
|
||||||
|
|
||||||
for field_name in expected['shown']:
|
|
||||||
assert field_name in output, (
|
|
||||||
f'{field_name!r} should appear in:\n'
|
|
||||||
f'{output}'
|
|
||||||
)
|
|
||||||
|
|
||||||
for field_name in expected['hidden']:
|
|
||||||
assert field_name not in output, (
|
|
||||||
f'{field_name!r} should NOT appear in:\n'
|
|
||||||
f'{output}'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_iter_ppfmt_lines_skips_private():
|
|
||||||
'''
|
|
||||||
Directly verify `iter_struct_ppfmt_lines()`
|
|
||||||
never yields tuples with `_`-prefixed field
|
|
||||||
names.
|
|
||||||
|
|
||||||
'''
|
|
||||||
struct = MixedFields()
|
|
||||||
lines: list[tuple[str, str]] = list(
|
|
||||||
iter_struct_ppfmt_lines(
|
|
||||||
struct,
|
|
||||||
field_indent=2,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
# should have lines for public fields only
|
|
||||||
assert len(lines) == 2
|
|
||||||
|
|
||||||
for _prefix, line_content in lines:
|
|
||||||
field_name: str = (
|
|
||||||
line_content.split(':')[0].strip()
|
|
||||||
)
|
|
||||||
assert not field_name.startswith('_'), (
|
|
||||||
f'private field leaked: {field_name!r}'
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_nested_struct_filters_inner_private():
|
|
||||||
'''
|
|
||||||
Verify that nested struct's private fields
|
|
||||||
are also filtered out during recursion.
|
|
||||||
|
|
||||||
'''
|
|
||||||
outer = Outer()
|
|
||||||
output: str = pformat(outer)
|
|
||||||
|
|
||||||
# outer's public field
|
|
||||||
assert 'label' in output
|
|
||||||
|
|
||||||
# inner's public field (recursed into)
|
|
||||||
assert 'x' in output
|
|
||||||
|
|
||||||
# inner's private field must be hidden
|
|
||||||
assert '_secret' not in output
|
|
||||||
|
|
||||||
|
|
||||||
def test_empty_struct_pformat():
|
|
||||||
'''
|
|
||||||
An empty struct should produce a valid
|
|
||||||
`pformat()` result with no field lines.
|
|
||||||
|
|
||||||
'''
|
|
||||||
output: str = pformat(EmptyStruct())
|
|
||||||
assert 'EmptyStruct(' in output
|
|
||||||
assert output.rstrip().endswith(')')
|
|
||||||
|
|
||||||
# no field lines => only struct header+footer
|
|
||||||
lines: list[tuple[str, str]] = list(
|
|
||||||
iter_struct_ppfmt_lines(
|
|
||||||
EmptyStruct(),
|
|
||||||
field_indent=2,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
assert lines == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_real_msgdec_pformat_hides_private():
|
|
||||||
'''
|
|
||||||
Verify `pformat()` on a real `MsgDec`
|
|
||||||
hides the `_dec` internal field.
|
|
||||||
|
|
||||||
NOTE: `MsgDec.__repr__` is custom and does
|
|
||||||
NOT call `pformat()`, so we call it directly.
|
|
||||||
|
|
||||||
'''
|
|
||||||
dec: MsgDec = mk_dec(spec=int)
|
|
||||||
output: str = pformat(dec)
|
|
||||||
|
|
||||||
# the private `_dec` field should be filtered
|
|
||||||
assert '_dec' not in output
|
|
||||||
|
|
||||||
# but the struct type name should be present
|
|
||||||
assert 'MsgDec(' in output
|
|
||||||
|
|
||||||
|
|
||||||
def test_pformat_repr_integration():
|
|
||||||
'''
|
|
||||||
Verify that `Struct.__repr__()` (which calls
|
|
||||||
`pformat()`) also hides private fields for
|
|
||||||
custom structs that do NOT override `__repr__`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
mixed = MixedFields()
|
|
||||||
output: str = repr(mixed)
|
|
||||||
|
|
||||||
assert 'name' in output
|
|
||||||
assert 'value' in output
|
|
||||||
assert '_hidden' not in output
|
|
||||||
assert '_meta' not in output
|
|
||||||
|
|
@ -1,12 +1,7 @@
|
||||||
'''
|
"""
|
||||||
Audit the simplest inter-actor bidirectional (streaming)
|
Bidirectional streaming.
|
||||||
msg patterns.
|
|
||||||
|
|
||||||
'''
|
"""
|
||||||
from __future__ import annotations
|
|
||||||
from typing import (
|
|
||||||
Callable,
|
|
||||||
)
|
|
||||||
import pytest
|
import pytest
|
||||||
import trio
|
import trio
|
||||||
import tractor
|
import tractor
|
||||||
|
|
@ -14,8 +9,10 @@ import tractor
|
||||||
|
|
||||||
@tractor.context
|
@tractor.context
|
||||||
async def simple_rpc(
|
async def simple_rpc(
|
||||||
|
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
data: int,
|
data: int,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Test a small ping-pong server.
|
Test a small ping-pong server.
|
||||||
|
|
@ -42,13 +39,15 @@ async def simple_rpc(
|
||||||
|
|
||||||
@tractor.context
|
@tractor.context
|
||||||
async def simple_rpc_with_forloop(
|
async def simple_rpc_with_forloop(
|
||||||
|
|
||||||
ctx: tractor.Context,
|
ctx: tractor.Context,
|
||||||
data: int,
|
data: int,
|
||||||
) -> None:
|
|
||||||
'''
|
|
||||||
Same as previous test but using `async for` syntax/api.
|
|
||||||
|
|
||||||
'''
|
) -> None:
|
||||||
|
"""Same as previous test but using ``async for`` syntax/api.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
# signal to parent that we're up
|
# signal to parent that we're up
|
||||||
await ctx.started(data + 1)
|
await ctx.started(data + 1)
|
||||||
|
|
||||||
|
|
@ -69,37 +68,21 @@ async def simple_rpc_with_forloop(
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'use_async_for',
|
'use_async_for',
|
||||||
[
|
[True, False],
|
||||||
True,
|
|
||||||
False,
|
|
||||||
],
|
|
||||||
ids='use_async_for={}'.format,
|
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'server_func',
|
'server_func',
|
||||||
[
|
[simple_rpc, simple_rpc_with_forloop],
|
||||||
simple_rpc,
|
|
||||||
simple_rpc_with_forloop,
|
|
||||||
],
|
|
||||||
ids='server_func={}'.format,
|
|
||||||
)
|
)
|
||||||
def test_simple_rpc(
|
def test_simple_rpc(server_func, use_async_for):
|
||||||
server_func: Callable,
|
|
||||||
use_async_for: bool,
|
|
||||||
loglevel: str,
|
|
||||||
debug_mode: bool,
|
|
||||||
):
|
|
||||||
'''
|
'''
|
||||||
The simplest request response pattern.
|
The simplest request response pattern.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
async def main():
|
async def main():
|
||||||
with trio.fail_after(6):
|
async with tractor.open_nursery() as n:
|
||||||
async with tractor.open_nursery(
|
|
||||||
loglevel=loglevel,
|
portal = await n.start_actor(
|
||||||
debug_mode=debug_mode,
|
|
||||||
) as an:
|
|
||||||
portal: tractor.Portal = await an.start_actor(
|
|
||||||
'rpc_server',
|
'rpc_server',
|
||||||
enable_modules=[__name__],
|
enable_modules=[__name__],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -98,8 +98,7 @@ def test_ipc_channel_break_during_stream(
|
||||||
expect_final_exc = TransportClosed
|
expect_final_exc = TransportClosed
|
||||||
|
|
||||||
mod: ModuleType = import_path(
|
mod: ModuleType = import_path(
|
||||||
examples_dir()
|
examples_dir() / 'advanced_faults'
|
||||||
/ 'advanced_faults'
|
|
||||||
/ 'ipc_failure_during_stream.py',
|
/ 'ipc_failure_during_stream.py',
|
||||||
root=examples_dir(),
|
root=examples_dir(),
|
||||||
consider_namespace_packages=False,
|
consider_namespace_packages=False,
|
||||||
|
|
@ -114,9 +113,8 @@ def test_ipc_channel_break_during_stream(
|
||||||
if (
|
if (
|
||||||
# only expect EoC if trans is broken on the child side,
|
# only expect EoC if trans is broken on the child side,
|
||||||
ipc_break['break_child_ipc_after'] is not False
|
ipc_break['break_child_ipc_after'] is not False
|
||||||
and
|
|
||||||
# AND we tell the child to call `MsgStream.aclose()`.
|
# AND we tell the child to call `MsgStream.aclose()`.
|
||||||
pre_aclose_msgstream
|
and pre_aclose_msgstream
|
||||||
):
|
):
|
||||||
# expect_final_exc = trio.EndOfChannel
|
# expect_final_exc = trio.EndOfChannel
|
||||||
# ^XXX NOPE! XXX^ since now `.open_stream()` absorbs this
|
# ^XXX NOPE! XXX^ since now `.open_stream()` absorbs this
|
||||||
|
|
@ -162,8 +160,7 @@ def test_ipc_channel_break_during_stream(
|
||||||
ipc_break['break_child_ipc_after'] is not False
|
ipc_break['break_child_ipc_after'] is not False
|
||||||
and (
|
and (
|
||||||
ipc_break['break_parent_ipc_after']
|
ipc_break['break_parent_ipc_after']
|
||||||
>
|
> ipc_break['break_child_ipc_after']
|
||||||
ipc_break['break_child_ipc_after']
|
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
if pre_aclose_msgstream:
|
if pre_aclose_msgstream:
|
||||||
|
|
@ -251,15 +248,8 @@ def test_ipc_channel_break_during_stream(
|
||||||
# get raw instance from pytest wrapper
|
# get raw instance from pytest wrapper
|
||||||
value = excinfo.value
|
value = excinfo.value
|
||||||
if isinstance(value, ExceptionGroup):
|
if isinstance(value, ExceptionGroup):
|
||||||
excs: tuple[Exception] = value.exceptions
|
excs = value.exceptions
|
||||||
assert (
|
assert len(excs) == 1
|
||||||
len(excs) <= 2
|
|
||||||
and
|
|
||||||
all(
|
|
||||||
isinstance(exc, TransportClosed)
|
|
||||||
for exc in excs
|
|
||||||
)
|
|
||||||
)
|
|
||||||
final_exc = excs[0]
|
final_exc = excs[0]
|
||||||
assert isinstance(final_exc, expect_final_exc)
|
assert isinstance(final_exc, expect_final_exc)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,8 @@ from tractor._testing import (
|
||||||
from .conftest import no_windows
|
from .conftest import no_windows
|
||||||
|
|
||||||
|
|
||||||
_non_linux: bool = platform.system() != 'Linux'
|
def is_win():
|
||||||
_friggin_windows: bool = platform.system() == 'Windows'
|
return platform.system() == 'Windows'
|
||||||
|
|
||||||
|
|
||||||
async def assert_err(delay=0):
|
async def assert_err(delay=0):
|
||||||
|
|
@ -431,7 +431,7 @@ async def test_nested_multierrors(loglevel, start_method):
|
||||||
for subexc in err.exceptions:
|
for subexc in err.exceptions:
|
||||||
|
|
||||||
# verify first level actor errors are wrapped as remote
|
# verify first level actor errors are wrapped as remote
|
||||||
if _friggin_windows:
|
if is_win():
|
||||||
|
|
||||||
# windows is often too slow and cancellation seems
|
# windows is often too slow and cancellation seems
|
||||||
# to happen before an actor is spawned
|
# to happen before an actor is spawned
|
||||||
|
|
@ -464,7 +464,7 @@ async def test_nested_multierrors(loglevel, start_method):
|
||||||
# XXX not sure what's up with this..
|
# XXX not sure what's up with this..
|
||||||
# on windows sometimes spawning is just too slow and
|
# on windows sometimes spawning is just too slow and
|
||||||
# we get back the (sent) cancel signal instead
|
# we get back the (sent) cancel signal instead
|
||||||
if _friggin_windows:
|
if is_win():
|
||||||
if isinstance(subexc, tractor.RemoteActorError):
|
if isinstance(subexc, tractor.RemoteActorError):
|
||||||
assert subexc.boxed_type in (
|
assert subexc.boxed_type in (
|
||||||
BaseExceptionGroup,
|
BaseExceptionGroup,
|
||||||
|
|
@ -507,22 +507,17 @@ def test_cancel_via_SIGINT(
|
||||||
|
|
||||||
@no_windows
|
@no_windows
|
||||||
def test_cancel_via_SIGINT_other_task(
|
def test_cancel_via_SIGINT_other_task(
|
||||||
loglevel: str,
|
loglevel,
|
||||||
start_method: str,
|
start_method,
|
||||||
spawn_backend: str,
|
spawn_backend,
|
||||||
):
|
):
|
||||||
'''
|
"""Ensure that a control-C (SIGINT) signal cancels both the parent
|
||||||
Ensure that a control-C (SIGINT) signal cancels both the parent
|
and child processes in trionic fashion even a subprocess is started
|
||||||
and child processes in trionic fashion even a subprocess is
|
from a seperate ``trio`` child task.
|
||||||
started from a seperate ``trio`` child task.
|
"""
|
||||||
|
pid = os.getpid()
|
||||||
'''
|
timeout: float = 2
|
||||||
pid: int = os.getpid()
|
if is_win(): # smh
|
||||||
timeout: float = (
|
|
||||||
4 if _non_linux
|
|
||||||
else 2
|
|
||||||
)
|
|
||||||
if _friggin_windows: # smh
|
|
||||||
timeout += 1
|
timeout += 1
|
||||||
|
|
||||||
async def spawn_and_sleep_forever(
|
async def spawn_and_sleep_forever(
|
||||||
|
|
@ -701,7 +696,7 @@ def test_fast_graceful_cancel_when_spawn_task_in_soft_proc_wait_for_daemon(
|
||||||
kbi_delay = 0.5
|
kbi_delay = 0.5
|
||||||
timeout: float = 2.9
|
timeout: float = 2.9
|
||||||
|
|
||||||
if _friggin_windows: # smh
|
if is_win(): # smh
|
||||||
timeout += 1
|
timeout += 1
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
|
|
|
||||||
|
|
@ -18,15 +18,16 @@ from tractor import RemoteActorError
|
||||||
|
|
||||||
|
|
||||||
async def aio_streamer(
|
async def aio_streamer(
|
||||||
chan: tractor.to_asyncio.LinkedTaskChannel,
|
from_trio: asyncio.Queue,
|
||||||
|
to_trio: trio.abc.SendChannel,
|
||||||
) -> trio.abc.ReceiveChannel:
|
) -> trio.abc.ReceiveChannel:
|
||||||
|
|
||||||
# required first msg to sync caller
|
# required first msg to sync caller
|
||||||
chan.started_nowait(None)
|
to_trio.send_nowait(None)
|
||||||
|
|
||||||
from itertools import cycle
|
from itertools import cycle
|
||||||
for i in cycle(range(10)):
|
for i in cycle(range(10)):
|
||||||
chan.send_nowait(i)
|
to_trio.send_nowait(i)
|
||||||
await asyncio.sleep(0.01)
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ from itertools import count
|
||||||
import math
|
import math
|
||||||
import platform
|
import platform
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
import sys
|
|
||||||
from typing import (
|
from typing import (
|
||||||
Callable,
|
Callable,
|
||||||
)
|
)
|
||||||
|
|
@ -942,11 +941,6 @@ def test_one_end_stream_not_opened(
|
||||||
from tractor._runtime import Actor
|
from tractor._runtime import Actor
|
||||||
buf_size = buf_size_increase + Actor.msg_buffer_size
|
buf_size = buf_size_increase + Actor.msg_buffer_size
|
||||||
|
|
||||||
timeout: float = (
|
|
||||||
1 if sys.platform == 'linux'
|
|
||||||
else 3
|
|
||||||
)
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
async with tractor.open_nursery(
|
async with tractor.open_nursery(
|
||||||
debug_mode=debug_mode,
|
debug_mode=debug_mode,
|
||||||
|
|
@ -956,7 +950,7 @@ def test_one_end_stream_not_opened(
|
||||||
enable_modules=[__name__],
|
enable_modules=[__name__],
|
||||||
)
|
)
|
||||||
|
|
||||||
with trio.fail_after(timeout):
|
with trio.fail_after(1):
|
||||||
async with portal.open_context(
|
async with portal.open_context(
|
||||||
entrypoint,
|
entrypoint,
|
||||||
) as (ctx, sent):
|
) as (ctx, sent):
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
"""
|
"""
|
||||||
Discovery subsys.
|
Actor "discovery" testing
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import platform
|
import platform
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import itertools
|
import itertools
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
import psutil
|
import psutil
|
||||||
import pytest
|
import pytest
|
||||||
|
|
@ -19,9 +17,7 @@ import trio
|
||||||
|
|
||||||
|
|
||||||
@tractor_test
|
@tractor_test
|
||||||
async def test_reg_then_unreg(
|
async def test_reg_then_unreg(reg_addr):
|
||||||
reg_addr: tuple,
|
|
||||||
):
|
|
||||||
actor = tractor.current_actor()
|
actor = tractor.current_actor()
|
||||||
assert actor.is_arbiter
|
assert actor.is_arbiter
|
||||||
assert len(actor._registry) == 1 # only self is registered
|
assert len(actor._registry) == 1 # only self is registered
|
||||||
|
|
@ -86,15 +82,11 @@ async def say_hello_use_wait(
|
||||||
|
|
||||||
|
|
||||||
@tractor_test
|
@tractor_test
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize('func', [say_hello, say_hello_use_wait])
|
||||||
'func',
|
|
||||||
[say_hello,
|
|
||||||
say_hello_use_wait]
|
|
||||||
)
|
|
||||||
async def test_trynamic_trio(
|
async def test_trynamic_trio(
|
||||||
func: Callable,
|
func,
|
||||||
start_method: str,
|
start_method,
|
||||||
reg_addr: tuple,
|
reg_addr,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Root actor acting as the "director" and running one-shot-task-actors
|
Root actor acting as the "director" and running one-shot-task-actors
|
||||||
|
|
@ -127,10 +119,7 @@ async def stream_forever():
|
||||||
await trio.sleep(0.01)
|
await trio.sleep(0.01)
|
||||||
|
|
||||||
|
|
||||||
async def cancel(
|
async def cancel(use_signal, delay=0):
|
||||||
use_signal: bool,
|
|
||||||
delay: float = 0,
|
|
||||||
):
|
|
||||||
# hold on there sally
|
# hold on there sally
|
||||||
await trio.sleep(delay)
|
await trio.sleep(delay)
|
||||||
|
|
||||||
|
|
@ -143,15 +132,13 @@ async def cancel(
|
||||||
raise KeyboardInterrupt
|
raise KeyboardInterrupt
|
||||||
|
|
||||||
|
|
||||||
async def stream_from(portal: tractor.Portal):
|
async def stream_from(portal):
|
||||||
async with portal.open_stream_from(stream_forever) as stream:
|
async with portal.open_stream_from(stream_forever) as stream:
|
||||||
async for value in stream:
|
async for value in stream:
|
||||||
print(value)
|
print(value)
|
||||||
|
|
||||||
|
|
||||||
async def unpack_reg(
|
async def unpack_reg(actor_or_portal):
|
||||||
actor_or_portal: tractor.Portal|tractor.Actor,
|
|
||||||
):
|
|
||||||
'''
|
'''
|
||||||
Get and unpack a "registry" RPC request from the "arbiter" registry
|
Get and unpack a "registry" RPC request from the "arbiter" registry
|
||||||
system.
|
system.
|
||||||
|
|
@ -186,9 +173,7 @@ async def spawn_and_check_registry(
|
||||||
registry_addrs=[reg_addr],
|
registry_addrs=[reg_addr],
|
||||||
debug_mode=debug_mode,
|
debug_mode=debug_mode,
|
||||||
):
|
):
|
||||||
async with tractor.get_registry(
|
async with tractor.get_registry(reg_addr) as portal:
|
||||||
addr=reg_addr,
|
|
||||||
) as portal:
|
|
||||||
# runtime needs to be up to call this
|
# runtime needs to be up to call this
|
||||||
actor = tractor.current_actor()
|
actor = tractor.current_actor()
|
||||||
|
|
||||||
|
|
@ -261,10 +246,10 @@ async def spawn_and_check_registry(
|
||||||
@pytest.mark.parametrize('with_streaming', [False, True])
|
@pytest.mark.parametrize('with_streaming', [False, True])
|
||||||
def test_subactors_unregister_on_cancel(
|
def test_subactors_unregister_on_cancel(
|
||||||
debug_mode: bool,
|
debug_mode: bool,
|
||||||
start_method: str,
|
start_method,
|
||||||
use_signal: bool,
|
use_signal,
|
||||||
reg_addr: tuple,
|
reg_addr,
|
||||||
with_streaming: bool,
|
with_streaming,
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Verify that cancelling a nursery results in all subactors
|
Verify that cancelling a nursery results in all subactors
|
||||||
|
|
@ -289,17 +274,15 @@ def test_subactors_unregister_on_cancel(
|
||||||
def test_subactors_unregister_on_cancel_remote_daemon(
|
def test_subactors_unregister_on_cancel_remote_daemon(
|
||||||
daemon: subprocess.Popen,
|
daemon: subprocess.Popen,
|
||||||
debug_mode: bool,
|
debug_mode: bool,
|
||||||
start_method: str,
|
start_method,
|
||||||
use_signal: bool,
|
use_signal,
|
||||||
reg_addr: tuple,
|
reg_addr,
|
||||||
with_streaming: bool,
|
with_streaming,
|
||||||
):
|
):
|
||||||
'''
|
"""Verify that cancelling a nursery results in all subactors
|
||||||
Verify that cancelling a nursery results in all subactors
|
deregistering themselves with a **remote** (not in the local process
|
||||||
deregistering themselves with a **remote** (not in the local
|
tree) arbiter.
|
||||||
process tree) arbiter.
|
"""
|
||||||
|
|
||||||
'''
|
|
||||||
with pytest.raises(KeyboardInterrupt):
|
with pytest.raises(KeyboardInterrupt):
|
||||||
trio.run(
|
trio.run(
|
||||||
partial(
|
partial(
|
||||||
|
|
@ -391,16 +374,14 @@ async def close_chans_before_nursery(
|
||||||
|
|
||||||
@pytest.mark.parametrize('use_signal', [False, True])
|
@pytest.mark.parametrize('use_signal', [False, True])
|
||||||
def test_close_channel_explicit(
|
def test_close_channel_explicit(
|
||||||
start_method: str,
|
start_method,
|
||||||
use_signal: bool,
|
use_signal,
|
||||||
reg_addr: tuple,
|
reg_addr,
|
||||||
):
|
):
|
||||||
'''
|
"""Verify that closing a stream explicitly and killing the actor's
|
||||||
Verify that closing a stream explicitly and killing the actor's
|
|
||||||
"root nursery" **before** the containing nursery tears down also
|
"root nursery" **before** the containing nursery tears down also
|
||||||
results in subactor(s) deregistering from the arbiter.
|
results in subactor(s) deregistering from the arbiter.
|
||||||
|
"""
|
||||||
'''
|
|
||||||
with pytest.raises(KeyboardInterrupt):
|
with pytest.raises(KeyboardInterrupt):
|
||||||
trio.run(
|
trio.run(
|
||||||
partial(
|
partial(
|
||||||
|
|
@ -415,16 +396,14 @@ def test_close_channel_explicit(
|
||||||
@pytest.mark.parametrize('use_signal', [False, True])
|
@pytest.mark.parametrize('use_signal', [False, True])
|
||||||
def test_close_channel_explicit_remote_arbiter(
|
def test_close_channel_explicit_remote_arbiter(
|
||||||
daemon: subprocess.Popen,
|
daemon: subprocess.Popen,
|
||||||
start_method: str,
|
start_method,
|
||||||
use_signal: bool,
|
use_signal,
|
||||||
reg_addr: tuple,
|
reg_addr,
|
||||||
):
|
):
|
||||||
'''
|
"""Verify that closing a stream explicitly and killing the actor's
|
||||||
Verify that closing a stream explicitly and killing the actor's
|
|
||||||
"root nursery" **before** the containing nursery tears down also
|
"root nursery" **before** the containing nursery tears down also
|
||||||
results in subactor(s) deregistering from the arbiter.
|
results in subactor(s) deregistering from the arbiter.
|
||||||
|
"""
|
||||||
'''
|
|
||||||
with pytest.raises(KeyboardInterrupt):
|
with pytest.raises(KeyboardInterrupt):
|
||||||
trio.run(
|
trio.run(
|
||||||
partial(
|
partial(
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,12 @@ import sys
|
||||||
import subprocess
|
import subprocess
|
||||||
import platform
|
import platform
|
||||||
import shutil
|
import shutil
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import tractor
|
|
||||||
from tractor._testing import (
|
from tractor._testing import (
|
||||||
examples_dir,
|
examples_dir,
|
||||||
)
|
)
|
||||||
|
|
||||||
_non_linux: bool = platform.system() != 'Linux'
|
|
||||||
_friggin_macos: bool = platform.system() == 'Darwin'
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def run_example_in_subproc(
|
def run_example_in_subproc(
|
||||||
|
|
@ -106,10 +101,8 @@ def run_example_in_subproc(
|
||||||
ids=lambda t: t[1],
|
ids=lambda t: t[1],
|
||||||
)
|
)
|
||||||
def test_example(
|
def test_example(
|
||||||
run_example_in_subproc: Callable,
|
run_example_in_subproc,
|
||||||
example_script: str,
|
example_script,
|
||||||
test_log: tractor.log.StackLevelAdapter,
|
|
||||||
ci_env: bool,
|
|
||||||
):
|
):
|
||||||
'''
|
'''
|
||||||
Load and run scripts from this repo's ``examples/`` dir as a user
|
Load and run scripts from this repo's ``examples/`` dir as a user
|
||||||
|
|
@ -123,32 +116,9 @@ def test_example(
|
||||||
'''
|
'''
|
||||||
ex_file: str = os.path.join(*example_script)
|
ex_file: str = os.path.join(*example_script)
|
||||||
|
|
||||||
if (
|
if 'rpc_bidir_streaming' in ex_file and sys.version_info < (3, 9):
|
||||||
'rpc_bidir_streaming' in ex_file
|
|
||||||
and
|
|
||||||
sys.version_info < (3, 9)
|
|
||||||
):
|
|
||||||
pytest.skip("2-way streaming example requires py3.9 async with syntax")
|
pytest.skip("2-way streaming example requires py3.9 async with syntax")
|
||||||
|
|
||||||
if (
|
|
||||||
'full_fledged_streaming_service' in ex_file
|
|
||||||
and
|
|
||||||
_friggin_macos
|
|
||||||
and
|
|
||||||
ci_env
|
|
||||||
):
|
|
||||||
pytest.skip(
|
|
||||||
'Streaming example is too flaky in CI\n'
|
|
||||||
'AND their competitor runs this CI service..\n'
|
|
||||||
'This test does run just fine "in person" however..'
|
|
||||||
)
|
|
||||||
|
|
||||||
timeout: float = (
|
|
||||||
60
|
|
||||||
if ci_env and _non_linux
|
|
||||||
else 16
|
|
||||||
)
|
|
||||||
|
|
||||||
with open(ex_file, 'r') as ex:
|
with open(ex_file, 'r') as ex:
|
||||||
code = ex.read()
|
code = ex.read()
|
||||||
|
|
||||||
|
|
@ -156,12 +126,9 @@ def test_example(
|
||||||
err = None
|
err = None
|
||||||
try:
|
try:
|
||||||
if not proc.poll():
|
if not proc.poll():
|
||||||
_, err = proc.communicate(timeout=timeout)
|
_, err = proc.communicate(timeout=15)
|
||||||
|
|
||||||
except subprocess.TimeoutExpired as e:
|
except subprocess.TimeoutExpired as e:
|
||||||
test_log.exception(
|
|
||||||
f'Example failed to finish within {timeout}s ??\n'
|
|
||||||
)
|
|
||||||
proc.kill()
|
proc.kill()
|
||||||
err = e.stderr
|
err = e.stderr
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,12 @@ async def sleep_and_err(
|
||||||
|
|
||||||
# just signature placeholders for compat with
|
# just signature placeholders for compat with
|
||||||
# ``to_asyncio.open_channel_from()``
|
# ``to_asyncio.open_channel_from()``
|
||||||
chan: to_asyncio.LinkedTaskChannel|None = None,
|
to_trio: trio.MemorySendChannel|None = None,
|
||||||
|
from_trio: asyncio.Queue|None = None,
|
||||||
|
|
||||||
):
|
):
|
||||||
if chan:
|
if to_trio:
|
||||||
chan.started_nowait('start')
|
to_trio.send_nowait('start')
|
||||||
|
|
||||||
await asyncio.sleep(sleep_for)
|
await asyncio.sleep(sleep_for)
|
||||||
assert 0
|
assert 0
|
||||||
|
|
@ -398,7 +399,7 @@ async def no_to_trio_in_args():
|
||||||
|
|
||||||
async def push_from_aio_task(
|
async def push_from_aio_task(
|
||||||
sequence: Iterable,
|
sequence: Iterable,
|
||||||
chan: to_asyncio.LinkedTaskChannel,
|
to_trio: trio.abc.SendChannel,
|
||||||
expect_cancel: False,
|
expect_cancel: False,
|
||||||
fail_early: bool,
|
fail_early: bool,
|
||||||
exit_early: bool,
|
exit_early: bool,
|
||||||
|
|
@ -406,12 +407,15 @@ async def push_from_aio_task(
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# print('trying breakpoint')
|
||||||
|
# breakpoint()
|
||||||
|
|
||||||
# sync caller ctx manager
|
# sync caller ctx manager
|
||||||
chan.started_nowait(True)
|
to_trio.send_nowait(True)
|
||||||
|
|
||||||
for i in sequence:
|
for i in sequence:
|
||||||
print(f'asyncio sending {i}')
|
print(f'asyncio sending {i}')
|
||||||
chan.send_nowait(i)
|
to_trio.send_nowait(i)
|
||||||
await asyncio.sleep(0.001)
|
await asyncio.sleep(0.001)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -728,21 +732,15 @@ def test_aio_errors_and_channel_propagates_and_closes(
|
||||||
|
|
||||||
|
|
||||||
async def aio_echo_server(
|
async def aio_echo_server(
|
||||||
chan: to_asyncio.LinkedTaskChannel,
|
to_trio: trio.MemorySendChannel,
|
||||||
|
from_trio: asyncio.Queue,
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
|
||||||
An IPC-msg "echo server" with msgs received and relayed by
|
|
||||||
a parent `trio.Task` into a child `asyncio.Task`
|
|
||||||
and then repeated back to that local parent (`trio.Task`)
|
|
||||||
and sent again back to the original calling remote actor.
|
|
||||||
|
|
||||||
'''
|
to_trio.send_nowait('start')
|
||||||
# same semantics as `trio.TaskStatus.started()`
|
|
||||||
chan.started_nowait('start')
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
msg = await chan.get()
|
msg = await from_trio.get()
|
||||||
except to_asyncio.TrioTaskExited:
|
except to_asyncio.TrioTaskExited:
|
||||||
print(
|
print(
|
||||||
'breaking aio echo loop due to `trio` exit!'
|
'breaking aio echo loop due to `trio` exit!'
|
||||||
|
|
@ -750,7 +748,7 @@ async def aio_echo_server(
|
||||||
break
|
break
|
||||||
|
|
||||||
# echo the msg back
|
# echo the msg back
|
||||||
chan.send_nowait(msg)
|
to_trio.send_nowait(msg)
|
||||||
|
|
||||||
# if we get the terminate sentinel
|
# if we get the terminate sentinel
|
||||||
# break the echo loop
|
# break the echo loop
|
||||||
|
|
@ -767,10 +765,7 @@ async def trio_to_aio_echo_server(
|
||||||
):
|
):
|
||||||
async with to_asyncio.open_channel_from(
|
async with to_asyncio.open_channel_from(
|
||||||
aio_echo_server,
|
aio_echo_server,
|
||||||
) as (
|
) as (first, chan):
|
||||||
first, # value from `chan.started_nowait()` above
|
|
||||||
chan,
|
|
||||||
):
|
|
||||||
assert first == 'start'
|
assert first == 'start'
|
||||||
|
|
||||||
await ctx.started(first)
|
await ctx.started(first)
|
||||||
|
|
@ -781,8 +776,7 @@ async def trio_to_aio_echo_server(
|
||||||
await chan.send(msg)
|
await chan.send(msg)
|
||||||
|
|
||||||
out = await chan.receive()
|
out = await chan.receive()
|
||||||
|
# echo back to parent actor-task
|
||||||
# echo back to parent-actor's remote parent-ctx-task!
|
|
||||||
await stream.send(out)
|
await stream.send(out)
|
||||||
|
|
||||||
if out is None:
|
if out is None:
|
||||||
|
|
@ -1096,21 +1090,24 @@ def test_sigint_closes_lifetime_stack(
|
||||||
|
|
||||||
|
|
||||||
# ?TODO asyncio.Task fn-deco?
|
# ?TODO asyncio.Task fn-deco?
|
||||||
|
# -[ ] do sig checkingat import time like @context?
|
||||||
|
# -[ ] maybe name it @aio_task ??
|
||||||
# -[ ] chan: to_asyncio.InterloopChannel ??
|
# -[ ] chan: to_asyncio.InterloopChannel ??
|
||||||
# -[ ] do fn-sig checking at import time like @context?
|
|
||||||
# |_[ ] maybe name it @a(sync)io_task ??
|
|
||||||
# @asyncio_task <- not bad ??
|
|
||||||
async def raise_before_started(
|
async def raise_before_started(
|
||||||
|
# from_trio: asyncio.Queue,
|
||||||
|
# to_trio: trio.abc.SendChannel,
|
||||||
chan: to_asyncio.LinkedTaskChannel,
|
chan: to_asyncio.LinkedTaskChannel,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
`asyncio.Task` entry point which RTEs before calling
|
`asyncio.Task` entry point which RTEs before calling
|
||||||
`chan.started_nowait()`.
|
`to_trio.send_nowait()`.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
raise RuntimeError('Some shite went wrong before `.send_nowait()`!!')
|
raise RuntimeError('Some shite went wrong before `.send_nowait()`!!')
|
||||||
|
|
||||||
|
# to_trio.send_nowait('Uhh we shouldve RTE-d ^^ ??')
|
||||||
chan.started_nowait('Uhh we shouldve RTE-d ^^ ??')
|
chan.started_nowait('Uhh we shouldve RTE-d ^^ ??')
|
||||||
await asyncio.sleep(float('inf'))
|
await asyncio.sleep(float('inf'))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,12 @@ import trio
|
||||||
import tractor
|
import tractor
|
||||||
from tractor import ( # typing
|
from tractor import ( # typing
|
||||||
Actor,
|
Actor,
|
||||||
Context,
|
|
||||||
ContextCancelled,
|
|
||||||
MsgStream,
|
|
||||||
Portal,
|
|
||||||
RemoteActorError,
|
|
||||||
current_actor,
|
current_actor,
|
||||||
open_nursery,
|
open_nursery,
|
||||||
|
Portal,
|
||||||
|
Context,
|
||||||
|
ContextCancelled,
|
||||||
|
RemoteActorError,
|
||||||
)
|
)
|
||||||
from tractor._testing import (
|
from tractor._testing import (
|
||||||
# tractor_test,
|
# tractor_test,
|
||||||
|
|
@ -797,8 +796,8 @@ async def basic_echo_server(
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Just the simplest `MsgStream` echo server which resays what you
|
Just the simplest `MsgStream` echo server which resays what
|
||||||
told it but with its uid in front ;)
|
you told it but with its uid in front ;)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
actor: Actor = tractor.current_actor()
|
actor: Actor = tractor.current_actor()
|
||||||
|
|
@ -967,14 +966,9 @@ async def tell_little_bro(
|
||||||
|
|
||||||
caller: str = '',
|
caller: str = '',
|
||||||
err_after: float|None = None,
|
err_after: float|None = None,
|
||||||
rng_seed: int = 100,
|
rng_seed: int = 50,
|
||||||
# NOTE, ensure ^ is large enough (on fast hw anyway)
|
|
||||||
# to ensure the peer cancel req arrives before the
|
|
||||||
# echoing dialog does itself Bp
|
|
||||||
):
|
):
|
||||||
# contact target actor, do a stream dialog.
|
# contact target actor, do a stream dialog.
|
||||||
lb: Portal
|
|
||||||
echo_ipc: MsgStream
|
|
||||||
async with (
|
async with (
|
||||||
tractor.wait_for_actor(
|
tractor.wait_for_actor(
|
||||||
name=actor_name
|
name=actor_name
|
||||||
|
|
@ -989,6 +983,7 @@ async def tell_little_bro(
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
) as (sub_ctx, first),
|
) as (sub_ctx, first),
|
||||||
|
|
||||||
sub_ctx.open_stream() as echo_ipc,
|
sub_ctx.open_stream() as echo_ipc,
|
||||||
):
|
):
|
||||||
actor: Actor = current_actor()
|
actor: Actor = current_actor()
|
||||||
|
|
@ -999,7 +994,6 @@ async def tell_little_bro(
|
||||||
i,
|
i,
|
||||||
)
|
)
|
||||||
await echo_ipc.send(msg)
|
await echo_ipc.send(msg)
|
||||||
await trio.sleep(0.001)
|
|
||||||
resp = await echo_ipc.receive()
|
resp = await echo_ipc.receive()
|
||||||
print(
|
print(
|
||||||
f'{caller} => {actor_name}: {msg}\n'
|
f'{caller} => {actor_name}: {msg}\n'
|
||||||
|
|
@ -1012,9 +1006,6 @@ async def tell_little_bro(
|
||||||
assert sub_uid != uid
|
assert sub_uid != uid
|
||||||
assert _i == i
|
assert _i == i
|
||||||
|
|
||||||
# XXX, usually should never get here!
|
|
||||||
# await tractor.pause()
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'raise_client_error',
|
'raise_client_error',
|
||||||
|
|
@ -1029,9 +1020,6 @@ def test_peer_spawns_and_cancels_service_subactor(
|
||||||
raise_client_error: str,
|
raise_client_error: str,
|
||||||
reg_addr: tuple[str, int],
|
reg_addr: tuple[str, int],
|
||||||
raise_sub_spawn_error_after: float|None,
|
raise_sub_spawn_error_after: float|None,
|
||||||
loglevel: str,
|
|
||||||
# ^XXX, set to 'warning' to see masked-exc warnings
|
|
||||||
# that may transpire during actor-nursery teardown.
|
|
||||||
):
|
):
|
||||||
# NOTE: this tests for the modden `mod wks open piker` bug
|
# NOTE: this tests for the modden `mod wks open piker` bug
|
||||||
# discovered as part of implementing workspace ctx
|
# discovered as part of implementing workspace ctx
|
||||||
|
|
@ -1061,7 +1049,6 @@ def test_peer_spawns_and_cancels_service_subactor(
|
||||||
# NOTE: to halt the peer tasks on ctxc, uncomment this.
|
# NOTE: to halt the peer tasks on ctxc, uncomment this.
|
||||||
debug_mode=debug_mode,
|
debug_mode=debug_mode,
|
||||||
registry_addrs=[reg_addr],
|
registry_addrs=[reg_addr],
|
||||||
loglevel=loglevel,
|
|
||||||
) as an:
|
) as an:
|
||||||
server: Portal = await an.start_actor(
|
server: Portal = await an.start_actor(
|
||||||
(server_name := 'spawn_server'),
|
(server_name := 'spawn_server'),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
"""
|
"""
|
||||||
Streaming via the, now legacy, "async-gen API".
|
Streaming via async gen api
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import platform
|
import platform
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
import trio
|
import trio
|
||||||
import tractor
|
import tractor
|
||||||
|
|
@ -21,11 +19,7 @@ def test_must_define_ctx():
|
||||||
async def no_ctx():
|
async def no_ctx():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
assert (
|
assert "no_ctx must be `ctx: tractor.Context" in str(err.value)
|
||||||
"no_ctx must be `ctx: tractor.Context"
|
|
||||||
in
|
|
||||||
str(err.value)
|
|
||||||
)
|
|
||||||
|
|
||||||
@tractor.stream
|
@tractor.stream
|
||||||
async def has_ctx(ctx):
|
async def has_ctx(ctx):
|
||||||
|
|
@ -75,14 +69,14 @@ async def stream_from_single_subactor(
|
||||||
async with tractor.open_nursery(
|
async with tractor.open_nursery(
|
||||||
registry_addrs=[reg_addr],
|
registry_addrs=[reg_addr],
|
||||||
start_method=start_method,
|
start_method=start_method,
|
||||||
) as an:
|
) as nursery:
|
||||||
|
|
||||||
async with tractor.find_actor('streamerd') as portals:
|
async with tractor.find_actor('streamerd') as portals:
|
||||||
|
|
||||||
if not portals:
|
if not portals:
|
||||||
|
|
||||||
# no brokerd actor found
|
# no brokerd actor found
|
||||||
portal = await an.start_actor(
|
portal = await nursery.start_actor(
|
||||||
'streamerd',
|
'streamerd',
|
||||||
enable_modules=[__name__],
|
enable_modules=[__name__],
|
||||||
)
|
)
|
||||||
|
|
@ -122,22 +116,11 @@ async def stream_from_single_subactor(
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'stream_func',
|
'stream_func', [async_gen_stream, context_stream]
|
||||||
[
|
|
||||||
async_gen_stream,
|
|
||||||
context_stream,
|
|
||||||
],
|
|
||||||
ids='stream_func={}'.format
|
|
||||||
)
|
)
|
||||||
def test_stream_from_single_subactor(
|
def test_stream_from_single_subactor(reg_addr, start_method, stream_func):
|
||||||
reg_addr: tuple,
|
"""Verify streaming from a spawned async generator.
|
||||||
start_method: str,
|
"""
|
||||||
stream_func: Callable,
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Verify streaming from a spawned async generator.
|
|
||||||
|
|
||||||
'''
|
|
||||||
trio.run(
|
trio.run(
|
||||||
partial(
|
partial(
|
||||||
stream_from_single_subactor,
|
stream_from_single_subactor,
|
||||||
|
|
@ -149,9 +132,10 @@ def test_stream_from_single_subactor(
|
||||||
|
|
||||||
|
|
||||||
# this is the first 2 actors, streamer_1 and streamer_2
|
# this is the first 2 actors, streamer_1 and streamer_2
|
||||||
async def stream_data(seed: int):
|
async def stream_data(seed):
|
||||||
|
|
||||||
for i in range(seed):
|
for i in range(seed):
|
||||||
|
|
||||||
yield i
|
yield i
|
||||||
|
|
||||||
# trigger scheduler to simulate practical usage
|
# trigger scheduler to simulate practical usage
|
||||||
|
|
@ -159,17 +143,15 @@ async def stream_data(seed: int):
|
||||||
|
|
||||||
|
|
||||||
# this is the third actor; the aggregator
|
# this is the third actor; the aggregator
|
||||||
async def aggregate(seed: int):
|
async def aggregate(seed):
|
||||||
'''
|
"""Ensure that the two streams we receive match but only stream
|
||||||
Ensure that the two streams we receive match but only stream
|
|
||||||
a single set of values to the parent.
|
a single set of values to the parent.
|
||||||
|
"""
|
||||||
'''
|
async with tractor.open_nursery() as nursery:
|
||||||
async with tractor.open_nursery() as an:
|
|
||||||
portals = []
|
portals = []
|
||||||
for i in range(1, 3):
|
for i in range(1, 3):
|
||||||
# fork point
|
# fork point
|
||||||
portal = await an.start_actor(
|
portal = await nursery.start_actor(
|
||||||
name=f'streamer_{i}',
|
name=f'streamer_{i}',
|
||||||
enable_modules=[__name__],
|
enable_modules=[__name__],
|
||||||
)
|
)
|
||||||
|
|
@ -182,8 +164,7 @@ async def aggregate(seed: int):
|
||||||
async with send_chan:
|
async with send_chan:
|
||||||
|
|
||||||
async with portal.open_stream_from(
|
async with portal.open_stream_from(
|
||||||
stream_data,
|
stream_data, seed=seed,
|
||||||
seed=seed,
|
|
||||||
) as stream:
|
) as stream:
|
||||||
|
|
||||||
async for value in stream:
|
async for value in stream:
|
||||||
|
|
@ -193,14 +174,10 @@ async def aggregate(seed: int):
|
||||||
print(f"FINISHED ITERATING {portal.channel.uid}")
|
print(f"FINISHED ITERATING {portal.channel.uid}")
|
||||||
|
|
||||||
# spawn 2 trio tasks to collect streams and push to a local queue
|
# spawn 2 trio tasks to collect streams and push to a local queue
|
||||||
async with trio.open_nursery() as tn:
|
async with trio.open_nursery() as n:
|
||||||
|
|
||||||
for portal in portals:
|
for portal in portals:
|
||||||
tn.start_soon(
|
n.start_soon(push_to_chan, portal, send_chan.clone())
|
||||||
push_to_chan,
|
|
||||||
portal,
|
|
||||||
send_chan.clone(),
|
|
||||||
)
|
|
||||||
|
|
||||||
# close this local task's reference to send side
|
# close this local task's reference to send side
|
||||||
await send_chan.aclose()
|
await send_chan.aclose()
|
||||||
|
|
@ -217,21 +194,20 @@ async def aggregate(seed: int):
|
||||||
|
|
||||||
print("FINISHED ITERATING in aggregator")
|
print("FINISHED ITERATING in aggregator")
|
||||||
|
|
||||||
await an.cancel()
|
await nursery.cancel()
|
||||||
print("WAITING on `ActorNursery` to finish")
|
print("WAITING on `ActorNursery` to finish")
|
||||||
print("AGGREGATOR COMPLETE!")
|
print("AGGREGATOR COMPLETE!")
|
||||||
|
|
||||||
|
|
||||||
async def a_quadruple_example() -> list[int]:
|
# this is the main actor and *arbiter*
|
||||||
'''
|
async def a_quadruple_example():
|
||||||
Open the root-actor which is also a "registrar".
|
# a nursery which spawns "actors"
|
||||||
|
async with tractor.open_nursery() as nursery:
|
||||||
|
|
||||||
'''
|
|
||||||
async with tractor.open_nursery() as an:
|
|
||||||
seed = int(1e3)
|
seed = int(1e3)
|
||||||
pre_start = time.time()
|
pre_start = time.time()
|
||||||
|
|
||||||
portal = await an.start_actor(
|
portal = await nursery.start_actor(
|
||||||
name='aggregator',
|
name='aggregator',
|
||||||
enable_modules=[__name__],
|
enable_modules=[__name__],
|
||||||
)
|
)
|
||||||
|
|
@ -252,14 +228,8 @@ async def a_quadruple_example() -> list[int]:
|
||||||
return result_stream
|
return result_stream
|
||||||
|
|
||||||
|
|
||||||
async def cancel_after(
|
async def cancel_after(wait, reg_addr):
|
||||||
wait: float,
|
async with tractor.open_root_actor(registry_addrs=[reg_addr]):
|
||||||
reg_addr: tuple,
|
|
||||||
) -> list[int]:
|
|
||||||
|
|
||||||
async with tractor.open_root_actor(
|
|
||||||
registry_addrs=[reg_addr],
|
|
||||||
):
|
|
||||||
with trio.move_on_after(wait):
|
with trio.move_on_after(wait):
|
||||||
return await a_quadruple_example()
|
return await a_quadruple_example()
|
||||||
|
|
||||||
|
|
@ -270,10 +240,6 @@ def time_quad_ex(
|
||||||
ci_env: bool,
|
ci_env: bool,
|
||||||
spawn_backend: str,
|
spawn_backend: str,
|
||||||
):
|
):
|
||||||
non_linux: bool = (_sys := platform.system()) != 'Linux'
|
|
||||||
if ci_env and non_linux:
|
|
||||||
pytest.skip(f'Test is too flaky on {_sys!r} in CI')
|
|
||||||
|
|
||||||
if spawn_backend == 'mp':
|
if spawn_backend == 'mp':
|
||||||
'''
|
'''
|
||||||
no idea but the mp *nix runs are flaking out here often...
|
no idea but the mp *nix runs are flaking out here often...
|
||||||
|
|
@ -281,20 +247,16 @@ def time_quad_ex(
|
||||||
'''
|
'''
|
||||||
pytest.skip("Test is too flaky on mp in CI")
|
pytest.skip("Test is too flaky on mp in CI")
|
||||||
|
|
||||||
timeout = 7 if non_linux else 4
|
timeout = 7 if platform.system() in ('Windows', 'Darwin') else 4
|
||||||
start = time.time()
|
start = time.time()
|
||||||
results: list[int] = trio.run(
|
results = trio.run(cancel_after, timeout, reg_addr)
|
||||||
cancel_after,
|
diff = time.time() - start
|
||||||
timeout,
|
|
||||||
reg_addr,
|
|
||||||
)
|
|
||||||
diff: float = time.time() - start
|
|
||||||
assert results
|
assert results
|
||||||
return results, diff
|
return results, diff
|
||||||
|
|
||||||
|
|
||||||
def test_a_quadruple_example(
|
def test_a_quadruple_example(
|
||||||
time_quad_ex: tuple[list[int], float],
|
time_quad_ex: tuple,
|
||||||
ci_env: bool,
|
ci_env: bool,
|
||||||
spawn_backend: str,
|
spawn_backend: str,
|
||||||
):
|
):
|
||||||
|
|
@ -302,12 +264,13 @@ def test_a_quadruple_example(
|
||||||
This also serves as a kind of "we'd like to be this fast test".
|
This also serves as a kind of "we'd like to be this fast test".
|
||||||
|
|
||||||
'''
|
'''
|
||||||
non_linux: bool = (_sys := platform.system()) != 'Linux'
|
|
||||||
|
|
||||||
results, diff = time_quad_ex
|
results, diff = time_quad_ex
|
||||||
assert results
|
assert results
|
||||||
this_fast = (
|
this_fast = (
|
||||||
6 if non_linux
|
6 if platform.system() in (
|
||||||
|
'Windows',
|
||||||
|
'Darwin',
|
||||||
|
)
|
||||||
else 3
|
else 3
|
||||||
)
|
)
|
||||||
assert diff < this_fast
|
assert diff < this_fast
|
||||||
|
|
@ -318,33 +281,19 @@ def test_a_quadruple_example(
|
||||||
list(map(lambda i: i/10, range(3, 9)))
|
list(map(lambda i: i/10, range(3, 9)))
|
||||||
)
|
)
|
||||||
def test_not_fast_enough_quad(
|
def test_not_fast_enough_quad(
|
||||||
reg_addr: tuple,
|
reg_addr, time_quad_ex, cancel_delay, ci_env, spawn_backend
|
||||||
time_quad_ex: tuple[list[int], float],
|
|
||||||
cancel_delay: float,
|
|
||||||
ci_env: bool,
|
|
||||||
spawn_backend: str,
|
|
||||||
):
|
):
|
||||||
'''
|
"""Verify we can cancel midway through the quad example and all actors
|
||||||
Verify we can cancel midway through the quad example and all
|
cancel gracefully.
|
||||||
actors cancel gracefully.
|
"""
|
||||||
|
|
||||||
'''
|
|
||||||
results, diff = time_quad_ex
|
results, diff = time_quad_ex
|
||||||
delay = max(diff - cancel_delay, 0)
|
delay = max(diff - cancel_delay, 0)
|
||||||
results = trio.run(
|
results = trio.run(cancel_after, delay, reg_addr)
|
||||||
cancel_after,
|
system = platform.system()
|
||||||
delay,
|
if system in ('Windows', 'Darwin') and results is not None:
|
||||||
reg_addr,
|
|
||||||
)
|
|
||||||
system: str = platform.system()
|
|
||||||
if (
|
|
||||||
system in ('Windows', 'Darwin')
|
|
||||||
and
|
|
||||||
results is not None
|
|
||||||
):
|
|
||||||
# In CI envoirments it seems later runs are quicker then the first
|
# In CI envoirments it seems later runs are quicker then the first
|
||||||
# so just ignore these
|
# so just ignore these
|
||||||
print(f'Woa there {system} caught your breath eh?')
|
print(f"Woa there {system} caught your breath eh?")
|
||||||
else:
|
else:
|
||||||
# should be cancelled mid-streaming
|
# should be cancelled mid-streaming
|
||||||
assert results is None
|
assert results is None
|
||||||
|
|
@ -352,24 +301,23 @@ def test_not_fast_enough_quad(
|
||||||
|
|
||||||
@tractor_test
|
@tractor_test
|
||||||
async def test_respawn_consumer_task(
|
async def test_respawn_consumer_task(
|
||||||
reg_addr: tuple,
|
reg_addr,
|
||||||
spawn_backend: str,
|
spawn_backend,
|
||||||
loglevel: str,
|
loglevel,
|
||||||
):
|
):
|
||||||
'''
|
"""Verify that ``._portal.ReceiveStream.shield()``
|
||||||
Verify that ``._portal.ReceiveStream.shield()``
|
|
||||||
sucessfully protects the underlying IPC channel from being closed
|
sucessfully protects the underlying IPC channel from being closed
|
||||||
when cancelling and respawning a consumer task.
|
when cancelling and respawning a consumer task.
|
||||||
|
|
||||||
This also serves to verify that all values from the stream can be
|
This also serves to verify that all values from the stream can be
|
||||||
received despite the respawns.
|
received despite the respawns.
|
||||||
|
|
||||||
'''
|
"""
|
||||||
stream = None
|
stream = None
|
||||||
|
|
||||||
async with tractor.open_nursery() as an:
|
async with tractor.open_nursery() as n:
|
||||||
|
|
||||||
portal = await an.start_actor(
|
portal = await n.start_actor(
|
||||||
name='streamer',
|
name='streamer',
|
||||||
enable_modules=[__name__]
|
enable_modules=[__name__]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,185 +0,0 @@
|
||||||
'''
|
|
||||||
`tractor.log`-wrapping unit tests.
|
|
||||||
|
|
||||||
'''
|
|
||||||
from pathlib import Path
|
|
||||||
import shutil
|
|
||||||
from types import ModuleType
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import tractor
|
|
||||||
from tractor import (
|
|
||||||
_code_load,
|
|
||||||
log,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_root_pkg_not_duplicated_in_logger_name():
|
|
||||||
'''
|
|
||||||
When both `pkg_name` and `name` are passed and they have
|
|
||||||
a common `<root_name>.< >` prefix, ensure that it is not
|
|
||||||
duplicated in the child's `StackLevelAdapter.name: str`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
project_name: str = 'pylib'
|
|
||||||
pkg_path: str = 'pylib.subpkg.mod'
|
|
||||||
|
|
||||||
assert not tractor.current_actor(
|
|
||||||
err_on_no_runtime=False,
|
|
||||||
)
|
|
||||||
proj_log = log.get_logger(
|
|
||||||
pkg_name=project_name,
|
|
||||||
mk_sublog=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
sublog = log.get_logger(
|
|
||||||
pkg_name=project_name,
|
|
||||||
name=pkg_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
assert proj_log is not sublog
|
|
||||||
assert sublog.name.count(proj_log.name) == 1
|
|
||||||
assert 'mod' not in sublog.name
|
|
||||||
|
|
||||||
|
|
||||||
def test_implicit_mod_name_applied_for_child(
|
|
||||||
testdir: pytest.Pytester,
|
|
||||||
loglevel: str,
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Verify that when `.log.get_logger(pkg_name='pylib')` is called
|
|
||||||
from a given sub-mod from within the `pylib` pkg-path, we
|
|
||||||
implicitly set the equiv of `name=__name__` from the caller's
|
|
||||||
module.
|
|
||||||
|
|
||||||
'''
|
|
||||||
# tractor.log.get_console_log(level=loglevel)
|
|
||||||
proj_name: str = 'snakelib'
|
|
||||||
mod_code: str = (
|
|
||||||
f'import tractor\n'
|
|
||||||
f'\n'
|
|
||||||
# if you need to trace `testdir` stuff @ import-time..
|
|
||||||
# f'breakpoint()\n'
|
|
||||||
f'log = tractor.log.get_logger(pkg_name="{proj_name}")\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
# create a sub-module for each pkg layer
|
|
||||||
_lib = testdir.mkpydir(proj_name)
|
|
||||||
pkg: Path = Path(_lib)
|
|
||||||
pkg_init_mod: Path = pkg / "__init__.py"
|
|
||||||
pkg_init_mod.write_text(mod_code)
|
|
||||||
|
|
||||||
subpkg: Path = pkg / 'subpkg'
|
|
||||||
subpkg.mkdir()
|
|
||||||
subpkgmod: Path = subpkg / "__init__.py"
|
|
||||||
subpkgmod.touch()
|
|
||||||
subpkgmod.write_text(mod_code)
|
|
||||||
|
|
||||||
_submod: Path = testdir.makepyfile(
|
|
||||||
_mod=mod_code,
|
|
||||||
)
|
|
||||||
|
|
||||||
pkg_submod = pkg / 'mod.py'
|
|
||||||
pkg_subpkg_submod = subpkg / 'submod.py'
|
|
||||||
shutil.copyfile(
|
|
||||||
_submod,
|
|
||||||
pkg_submod,
|
|
||||||
)
|
|
||||||
shutil.copyfile(
|
|
||||||
_submod,
|
|
||||||
pkg_subpkg_submod,
|
|
||||||
)
|
|
||||||
testdir.chdir()
|
|
||||||
# NOTE, to introspect the py-file-module-layout use (in .xsh
|
|
||||||
# syntax): `ranger @str(testdir)`
|
|
||||||
|
|
||||||
# XXX NOTE, once the "top level" pkg mod has been
|
|
||||||
# imported, we can then use `import` syntax to
|
|
||||||
# import it's sub-pkgs and modules.
|
|
||||||
subpkgmod: ModuleType = _code_load.load_module_from_path(
|
|
||||||
Path(pkg / '__init__.py'),
|
|
||||||
module_name=proj_name,
|
|
||||||
)
|
|
||||||
|
|
||||||
pkg_root_log = log.get_logger(
|
|
||||||
pkg_name=proj_name,
|
|
||||||
mk_sublog=False,
|
|
||||||
)
|
|
||||||
# the top level pkg-mod, created just now,
|
|
||||||
# by above API call.
|
|
||||||
assert pkg_root_log.name == proj_name
|
|
||||||
assert not pkg_root_log.logger.getChildren()
|
|
||||||
#
|
|
||||||
# ^TODO! test this same output but created via a `get_logger()`
|
|
||||||
# call in the `snakelib.__init__py`!!
|
|
||||||
|
|
||||||
# NOTE, the pkg-level "init mod" should of course
|
|
||||||
# have the same name as the package ns-path.
|
|
||||||
import snakelib as init_mod
|
|
||||||
assert init_mod.log.name == proj_name
|
|
||||||
|
|
||||||
# NOTE, a first-pkg-level sub-module should only
|
|
||||||
# use the package-name since the leaf-node-module
|
|
||||||
# will be included in log headers by default.
|
|
||||||
from snakelib import mod
|
|
||||||
assert mod.log.name == proj_name
|
|
||||||
|
|
||||||
from snakelib import subpkg
|
|
||||||
assert (
|
|
||||||
subpkg.log.name
|
|
||||||
==
|
|
||||||
subpkg.__package__
|
|
||||||
==
|
|
||||||
f'{proj_name}.subpkg'
|
|
||||||
)
|
|
||||||
|
|
||||||
from snakelib.subpkg import submod
|
|
||||||
assert (
|
|
||||||
submod.log.name
|
|
||||||
==
|
|
||||||
submod.__package__
|
|
||||||
==
|
|
||||||
f'{proj_name}.subpkg'
|
|
||||||
)
|
|
||||||
|
|
||||||
sub_logs = pkg_root_log.logger.getChildren()
|
|
||||||
assert len(sub_logs) == 1 # only one nested sub-pkg module
|
|
||||||
assert submod.log.logger in sub_logs
|
|
||||||
|
|
||||||
|
|
||||||
# TODO, moar tests against existing feats:
|
|
||||||
# ------ - ------
|
|
||||||
# - [ ] color settings?
|
|
||||||
# - [ ] header contents like,
|
|
||||||
# - actor + thread + task names from various conc-primitives,
|
|
||||||
# - [ ] `StackLevelAdapter` extensions,
|
|
||||||
# - our custom levels/methods: `transport|runtime|cance|pdb|devx`
|
|
||||||
# - [ ] custom-headers support?
|
|
||||||
#
|
|
||||||
|
|
||||||
# TODO, test driven dev of new-ideas/long-wanted feats,
|
|
||||||
# ------ - ------
|
|
||||||
# - [ ] https://github.com/goodboy/tractor/issues/244
|
|
||||||
# - [ ] @catern mentioned using a sync / deterministic sys
|
|
||||||
# and in particular `svlogd`?
|
|
||||||
# |_ https://smarden.org/runit/svlogd.8
|
|
||||||
|
|
||||||
# - [ ] using adapter vs. filters?
|
|
||||||
# - https://stackoverflow.com/questions/60691759/add-information-to-every-log-message-in-python-logging/61830838#61830838
|
|
||||||
|
|
||||||
# - [ ] `.at_least_level()` optimization which short circuits wtv
|
|
||||||
# `logging` is doing behind the scenes when the level filters
|
|
||||||
# the emission..?
|
|
||||||
|
|
||||||
# - [ ] use of `.log.get_console_log()` in subactors and the
|
|
||||||
# subtleties of ensuring it actually emits from a subproc.
|
|
||||||
|
|
||||||
# - [ ] this idea of activating per-subsys emissions with some
|
|
||||||
# kind of `.name` filter passed to the runtime or maybe configured
|
|
||||||
# via the root `StackLevelAdapter`?
|
|
||||||
|
|
||||||
# - [ ] use of `logging.dict.dictConfig()` to simplify the impl
|
|
||||||
# of any of ^^ ??
|
|
||||||
# - https://stackoverflow.com/questions/7507825/where-is-a-complete-example-of-logging-config-dictconfig
|
|
||||||
# - https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema
|
|
||||||
# - https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
|
|
||||||
|
|
@ -1,13 +1,8 @@
|
||||||
"""
|
"""
|
||||||
Multiple python programs invoking the runtime.
|
Multiple python programs invoking the runtime.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
|
||||||
import platform
|
import platform
|
||||||
import subprocess
|
|
||||||
import time
|
import time
|
||||||
from typing import (
|
|
||||||
TYPE_CHECKING,
|
|
||||||
)
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import trio
|
import trio
|
||||||
|
|
@ -15,32 +10,14 @@ import tractor
|
||||||
from tractor._testing import (
|
from tractor._testing import (
|
||||||
tractor_test,
|
tractor_test,
|
||||||
)
|
)
|
||||||
from tractor import (
|
|
||||||
current_actor,
|
|
||||||
_state,
|
|
||||||
Actor,
|
|
||||||
Context,
|
|
||||||
Portal,
|
|
||||||
)
|
|
||||||
from .conftest import (
|
from .conftest import (
|
||||||
sig_prog,
|
sig_prog,
|
||||||
_INT_SIGNAL,
|
_INT_SIGNAL,
|
||||||
_INT_RETURN_CODE,
|
_INT_RETURN_CODE,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from tractor.msg import Aid
|
|
||||||
from tractor._addr import (
|
|
||||||
UnwrappedAddress,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
def test_abort_on_sigint(daemon):
|
||||||
_non_linux: bool = platform.system() != 'Linux'
|
|
||||||
|
|
||||||
|
|
||||||
def test_abort_on_sigint(
|
|
||||||
daemon: subprocess.Popen,
|
|
||||||
):
|
|
||||||
assert daemon.returncode is None
|
assert daemon.returncode is None
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
sig_prog(daemon, _INT_SIGNAL)
|
sig_prog(daemon, _INT_SIGNAL)
|
||||||
|
|
@ -53,11 +30,8 @@ def test_abort_on_sigint(
|
||||||
|
|
||||||
|
|
||||||
@tractor_test
|
@tractor_test
|
||||||
async def test_cancel_remote_arbiter(
|
async def test_cancel_remote_arbiter(daemon, reg_addr):
|
||||||
daemon: subprocess.Popen,
|
assert not tractor.current_actor().is_arbiter
|
||||||
reg_addr: UnwrappedAddress,
|
|
||||||
):
|
|
||||||
assert not current_actor().is_arbiter
|
|
||||||
async with tractor.get_registry(reg_addr) as portal:
|
async with tractor.get_registry(reg_addr) as portal:
|
||||||
await portal.cancel_actor()
|
await portal.cancel_actor()
|
||||||
|
|
||||||
|
|
@ -71,113 +45,24 @@ async def test_cancel_remote_arbiter(
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def test_register_duplicate_name(
|
def test_register_duplicate_name(daemon, reg_addr):
|
||||||
daemon: subprocess.Popen,
|
|
||||||
reg_addr: UnwrappedAddress,
|
|
||||||
):
|
|
||||||
async def main():
|
async def main():
|
||||||
|
|
||||||
async with tractor.open_nursery(
|
async with tractor.open_nursery(
|
||||||
registry_addrs=[reg_addr],
|
registry_addrs=[reg_addr],
|
||||||
) as an:
|
) as n:
|
||||||
|
|
||||||
assert not current_actor().is_arbiter
|
assert not tractor.current_actor().is_arbiter
|
||||||
|
|
||||||
p1 = await an.start_actor('doggy')
|
p1 = await n.start_actor('doggy')
|
||||||
p2 = await an.start_actor('doggy')
|
p2 = await n.start_actor('doggy')
|
||||||
|
|
||||||
async with tractor.wait_for_actor('doggy') as portal:
|
async with tractor.wait_for_actor('doggy') as portal:
|
||||||
assert portal.channel.uid in (p2.channel.uid, p1.channel.uid)
|
assert portal.channel.uid in (p2.channel.uid, p1.channel.uid)
|
||||||
|
|
||||||
await an.cancel()
|
await n.cancel()
|
||||||
|
|
||||||
# XXX, run manually since we want to start this root **after**
|
# run it manually since we want to start **after**
|
||||||
# the other "daemon" program with it's own root.
|
# the other "daemon" program
|
||||||
trio.run(main)
|
|
||||||
|
|
||||||
|
|
||||||
@tractor.context
|
|
||||||
async def get_root_portal(
|
|
||||||
ctx: Context,
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Connect back to the root actor manually (using `._discovery` API)
|
|
||||||
and ensure it's contact info is the same as our immediate parent.
|
|
||||||
|
|
||||||
'''
|
|
||||||
sub: Actor = current_actor()
|
|
||||||
rtvs: dict = _state._runtime_vars
|
|
||||||
raddrs: list[UnwrappedAddress] = rtvs['_root_addrs']
|
|
||||||
|
|
||||||
# await tractor.pause()
|
|
||||||
# XXX, in case the sub->root discovery breaks you might need
|
|
||||||
# this (i know i did Xp)!!
|
|
||||||
# from tractor.devx import mk_pdb
|
|
||||||
# mk_pdb().set_trace()
|
|
||||||
|
|
||||||
assert (
|
|
||||||
len(raddrs) == 1
|
|
||||||
and
|
|
||||||
list(sub._parent_chan.raddr.unwrap()) in raddrs
|
|
||||||
)
|
|
||||||
|
|
||||||
# connect back to our immediate parent which should also
|
|
||||||
# be the actor-tree's root.
|
|
||||||
from tractor._discovery import get_root
|
|
||||||
ptl: Portal
|
|
||||||
async with get_root() as ptl:
|
|
||||||
root_aid: Aid = ptl.chan.aid
|
|
||||||
parent_ptl: Portal = current_actor().get_parent()
|
|
||||||
assert (
|
|
||||||
root_aid.name == 'root'
|
|
||||||
and
|
|
||||||
parent_ptl.chan.aid == root_aid
|
|
||||||
)
|
|
||||||
await ctx.started()
|
|
||||||
|
|
||||||
|
|
||||||
def test_non_registrar_spawns_child(
|
|
||||||
daemon: subprocess.Popen,
|
|
||||||
reg_addr: UnwrappedAddress,
|
|
||||||
loglevel: str,
|
|
||||||
debug_mode: bool,
|
|
||||||
ci_env: bool,
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Ensure a non-regristar (serving) root actor can spawn a sub and
|
|
||||||
that sub can connect back (manually) to it's rent that is the
|
|
||||||
root without issue.
|
|
||||||
|
|
||||||
More or less this audits the global contact info in
|
|
||||||
`._state._runtime_vars`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
async def main():
|
|
||||||
|
|
||||||
# XXX, since apparently on macos in GH's CI it can be a race
|
|
||||||
# with the `daemon` registrar on grabbing the socket-addr..
|
|
||||||
if ci_env and _non_linux:
|
|
||||||
await trio.sleep(.5)
|
|
||||||
|
|
||||||
async with tractor.open_nursery(
|
|
||||||
registry_addrs=[reg_addr],
|
|
||||||
loglevel=loglevel,
|
|
||||||
debug_mode=debug_mode,
|
|
||||||
) as an:
|
|
||||||
|
|
||||||
actor: Actor = tractor.current_actor()
|
|
||||||
assert not actor.is_registrar
|
|
||||||
sub_ptl: Portal = await an.start_actor(
|
|
||||||
name='sub',
|
|
||||||
enable_modules=[__name__],
|
|
||||||
)
|
|
||||||
|
|
||||||
async with sub_ptl.open_context(
|
|
||||||
get_root_portal,
|
|
||||||
) as (ctx, _):
|
|
||||||
print('Waiting for `sub` to connect back to us..')
|
|
||||||
|
|
||||||
await an.cancel()
|
|
||||||
|
|
||||||
# XXX, run manually since we want to start this root **after**
|
|
||||||
# the other "daemon" program with it's own root.
|
|
||||||
trio.run(main)
|
trio.run(main)
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ from tractor.log import (
|
||||||
get_console_log,
|
get_console_log,
|
||||||
get_logger,
|
get_logger,
|
||||||
)
|
)
|
||||||
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
log = get_logger()
|
|
||||||
|
|
||||||
_resource: int = 0
|
_resource: int = 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,12 +91,13 @@ def test_infected_root_actor(
|
||||||
async def sync_and_err(
|
async def sync_and_err(
|
||||||
# just signature placeholders for compat with
|
# just signature placeholders for compat with
|
||||||
# ``to_asyncio.open_channel_from()``
|
# ``to_asyncio.open_channel_from()``
|
||||||
chan: tractor.to_asyncio.LinkedTaskChannel,
|
to_trio: trio.MemorySendChannel,
|
||||||
|
from_trio: asyncio.Queue,
|
||||||
ev: asyncio.Event,
|
ev: asyncio.Event,
|
||||||
|
|
||||||
):
|
):
|
||||||
if chan:
|
if to_trio:
|
||||||
chan.started_nowait('start')
|
to_trio.send_nowait('start')
|
||||||
|
|
||||||
await ev.wait()
|
await ev.wait()
|
||||||
raise RuntimeError('asyncio-side')
|
raise RuntimeError('asyncio-side')
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
Shared mem primitives and APIs.
|
Shared mem primitives and APIs.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import platform
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
# import numpy
|
# import numpy
|
||||||
|
|
@ -54,18 +53,7 @@ def test_child_attaches_alot():
|
||||||
shm_key=shml.key,
|
shm_key=shml.key,
|
||||||
) as (ctx, start_val),
|
) as (ctx, start_val),
|
||||||
):
|
):
|
||||||
assert (_key := shml.key) == start_val
|
assert start_val == key
|
||||||
|
|
||||||
if platform.system() != 'Darwin':
|
|
||||||
# XXX, macOS has a char limit..
|
|
||||||
# see `ipc._shm._shorten_key_for_macos`
|
|
||||||
assert (
|
|
||||||
start_val
|
|
||||||
==
|
|
||||||
key
|
|
||||||
==
|
|
||||||
_key
|
|
||||||
)
|
|
||||||
await ctx.result()
|
await ctx.result()
|
||||||
|
|
||||||
await portal.cancel_actor()
|
await portal.cancel_actor()
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ from .ipc._uds import UDSAddress
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._runtime import Actor
|
from ._runtime import Actor
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# TODO, maybe breakout the netns key to a struct?
|
# TODO, maybe breakout the netns key to a struct?
|
||||||
|
|
@ -259,8 +259,6 @@ def wrap_address(
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
# import pdbp; pdbp.set_trace()
|
# import pdbp; pdbp.set_trace()
|
||||||
# from tractor.devx import mk_pdb
|
|
||||||
# mk_pdb().set_trace()
|
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
f'Can not wrap unwrapped-address ??\n'
|
f'Can not wrap unwrapped-address ??\n'
|
||||||
f'type(addr): {type(addr)!r}\n'
|
f'type(addr): {type(addr)!r}\n'
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
# tractor: structured concurrent "actors".
|
|
||||||
# Copyright 2018-eternity Tyler Goodlet.
|
|
||||||
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU Affero General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
'''
|
|
||||||
(Hot) coad (re-)load utils for python.
|
|
||||||
|
|
||||||
'''
|
|
||||||
import importlib
|
|
||||||
from pathlib import Path
|
|
||||||
import sys
|
|
||||||
from types import ModuleType
|
|
||||||
|
|
||||||
# ?TODO, move this into internal libs?
|
|
||||||
# -[ ] we already use it in `modden.config._pymod` as well
|
|
||||||
def load_module_from_path(
|
|
||||||
path: Path,
|
|
||||||
module_name: str|None = None,
|
|
||||||
) -> ModuleType:
|
|
||||||
'''
|
|
||||||
Taken from SO,
|
|
||||||
https://stackoverflow.com/a/67208147
|
|
||||||
|
|
||||||
which is based on stdlib docs,
|
|
||||||
https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly
|
|
||||||
|
|
||||||
'''
|
|
||||||
module_name = module_name or path.stem
|
|
||||||
spec = importlib.util.spec_from_file_location(
|
|
||||||
module_name,
|
|
||||||
str(path),
|
|
||||||
)
|
|
||||||
module = importlib.util.module_from_spec(spec)
|
|
||||||
sys.modules[module_name] = module
|
|
||||||
spec.loader.exec_module(module)
|
|
||||||
return module
|
|
||||||
|
|
@ -70,7 +70,6 @@ from ._exceptions import (
|
||||||
MsgTypeError,
|
MsgTypeError,
|
||||||
RemoteActorError,
|
RemoteActorError,
|
||||||
StreamOverrun,
|
StreamOverrun,
|
||||||
TransportClosed,
|
|
||||||
pack_from_raise,
|
pack_from_raise,
|
||||||
unpack_error,
|
unpack_error,
|
||||||
)
|
)
|
||||||
|
|
@ -114,7 +113,7 @@ if TYPE_CHECKING:
|
||||||
CallerInfo,
|
CallerInfo,
|
||||||
)
|
)
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Unresolved:
|
class Unresolved:
|
||||||
|
|
@ -2392,18 +2391,16 @@ async def open_context_from_portal(
|
||||||
case trio.Cancelled():
|
case trio.Cancelled():
|
||||||
logmeth = log.cancel
|
logmeth = log.cancel
|
||||||
cause: str = 'cancelled'
|
cause: str = 'cancelled'
|
||||||
msg: str = (
|
|
||||||
f'ctx {ctx.side!r}-side {cause!r} with,\n'
|
|
||||||
f'{ctx.repr_outcome()!r}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
# XXX explicitly report on any non-graceful-taskc cases
|
# XXX explicitly report on any non-graceful-taskc cases
|
||||||
case _:
|
case _:
|
||||||
cause: str = 'errored'
|
cause: str = 'errored'
|
||||||
logmeth = log.exception
|
logmeth = log.exception
|
||||||
msg: str = f'ctx {ctx.side!r}-side {cause!r} with,\n'
|
|
||||||
|
|
||||||
logmeth(msg)
|
logmeth(
|
||||||
|
f'ctx {ctx.side!r}-side {cause!r} with,\n'
|
||||||
|
f'{ctx.repr_outcome()!r}\n'
|
||||||
|
)
|
||||||
|
|
||||||
if debug_mode():
|
if debug_mode():
|
||||||
# async with debug.acquire_debug_lock(portal.actor.uid):
|
# async with debug.acquire_debug_lock(portal.actor.uid):
|
||||||
|
|
@ -2429,7 +2426,10 @@ async def open_context_from_portal(
|
||||||
try:
|
try:
|
||||||
# await pause(shield=True)
|
# await pause(shield=True)
|
||||||
await ctx.cancel()
|
await ctx.cancel()
|
||||||
except TransportClosed:
|
except (
|
||||||
|
trio.BrokenResourceError,
|
||||||
|
trio.ClosedResourceError,
|
||||||
|
):
|
||||||
log.warning(
|
log.warning(
|
||||||
'IPC connection for context is broken?\n'
|
'IPC connection for context is broken?\n'
|
||||||
f'task: {ctx.cid}\n'
|
f'task: {ctx.cid}\n'
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ if TYPE_CHECKING:
|
||||||
from ._runtime import Actor
|
from ._runtime import Actor
|
||||||
|
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
|
|
@ -91,13 +91,10 @@ async def get_registry(
|
||||||
|
|
||||||
|
|
||||||
@acm
|
@acm
|
||||||
async def get_root(**kwargs) -> AsyncGenerator[Portal, None]:
|
async def get_root(
|
||||||
'''
|
**kwargs,
|
||||||
Deliver the current actor's "root process" actor (yes in actor
|
) -> AsyncGenerator[Portal, None]:
|
||||||
and proc tree terms) by delivering a `Portal` from the spawn-time
|
|
||||||
provided contact address.
|
|
||||||
|
|
||||||
'''
|
|
||||||
# TODO: rename mailbox to `_root_maddr` when we finally
|
# TODO: rename mailbox to `_root_maddr` when we finally
|
||||||
# add and impl libp2p multi-addrs?
|
# add and impl libp2p multi-addrs?
|
||||||
addr = _runtime_vars['_root_mailbox']
|
addr = _runtime_vars['_root_mailbox']
|
||||||
|
|
@ -196,11 +193,6 @@ async def maybe_open_portal(
|
||||||
addr: UnwrappedAddress,
|
addr: UnwrappedAddress,
|
||||||
name: str,
|
name: str,
|
||||||
):
|
):
|
||||||
'''
|
|
||||||
Open a `Portal` to the actor serving @ `addr` or `None` if no
|
|
||||||
peer can be contacted or found.
|
|
||||||
|
|
||||||
'''
|
|
||||||
async with query_actor(
|
async with query_actor(
|
||||||
name=name,
|
name=name,
|
||||||
regaddr=addr,
|
regaddr=addr,
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ if TYPE_CHECKING:
|
||||||
from ._spawn import SpawnMethodKey
|
from ._spawn import SpawnMethodKey
|
||||||
|
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _mp_main(
|
def _mp_main(
|
||||||
|
|
@ -72,15 +72,11 @@ def _mp_main(
|
||||||
spawn_ctx: mp.context.BaseContext = try_set_start_method(start_method)
|
spawn_ctx: mp.context.BaseContext = try_set_start_method(start_method)
|
||||||
assert spawn_ctx
|
assert spawn_ctx
|
||||||
|
|
||||||
# XXX, enable root log at level
|
|
||||||
if actor.loglevel is not None:
|
if actor.loglevel is not None:
|
||||||
log.info(
|
log.info(
|
||||||
f'Setting loglevel for {actor.uid} to {actor.loglevel!r}'
|
f'Setting loglevel for {actor.uid} to {actor.loglevel}'
|
||||||
)
|
|
||||||
get_console_log(
|
|
||||||
level=actor.loglevel,
|
|
||||||
name='tractor',
|
|
||||||
)
|
)
|
||||||
|
get_console_log(actor.loglevel)
|
||||||
|
|
||||||
# TODO: use scops headers like for `trio` below!
|
# TODO: use scops headers like for `trio` below!
|
||||||
# (well after we libify it maybe..)
|
# (well after we libify it maybe..)
|
||||||
|
|
@ -130,12 +126,8 @@ def _trio_main(
|
||||||
parent_addr=parent_addr
|
parent_addr=parent_addr
|
||||||
)
|
)
|
||||||
|
|
||||||
# XXX, enable root log at level
|
|
||||||
if actor.loglevel is not None:
|
if actor.loglevel is not None:
|
||||||
get_console_log(
|
get_console_log(actor.loglevel)
|
||||||
level=actor.loglevel,
|
|
||||||
name='tractor',
|
|
||||||
)
|
|
||||||
log.info(
|
log.info(
|
||||||
f'Starting `trio` subactor from parent @ '
|
f'Starting `trio` subactor from parent @ '
|
||||||
f'{parent_addr}\n'
|
f'{parent_addr}\n'
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ from ._streaming import (
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._runtime import Actor
|
from ._runtime import Actor
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Portal:
|
class Portal:
|
||||||
|
|
@ -329,7 +329,18 @@ class Portal:
|
||||||
# if we get here some weird cancellation case happened
|
# if we get here some weird cancellation case happened
|
||||||
return False
|
return False
|
||||||
|
|
||||||
except TransportClosed as tpt_err:
|
except (
|
||||||
|
# XXX, should never really get raised unless we aren't
|
||||||
|
# wrapping them in the below type by mistake?
|
||||||
|
#
|
||||||
|
# Leaving the catch here for now until we're very sure
|
||||||
|
# all the cases (for various tpt protos) have indeed been
|
||||||
|
# re-wrapped ;p
|
||||||
|
trio.ClosedResourceError,
|
||||||
|
trio.BrokenResourceError,
|
||||||
|
|
||||||
|
TransportClosed,
|
||||||
|
) as tpt_err:
|
||||||
ipc_borked_report: str = (
|
ipc_borked_report: str = (
|
||||||
f'IPC for actor already closed/broken?\n\n'
|
f'IPC for actor already closed/broken?\n\n'
|
||||||
f'\n'
|
f'\n'
|
||||||
|
|
|
||||||
|
|
@ -88,8 +88,7 @@ async def maybe_block_bp(
|
||||||
bp_blocked: bool
|
bp_blocked: bool
|
||||||
if (
|
if (
|
||||||
debug_mode
|
debug_mode
|
||||||
and
|
and maybe_enable_greenback
|
||||||
maybe_enable_greenback
|
|
||||||
and (
|
and (
|
||||||
maybe_mod := await debug.maybe_init_greenback(
|
maybe_mod := await debug.maybe_init_greenback(
|
||||||
raise_not_found=False,
|
raise_not_found=False,
|
||||||
|
|
@ -290,12 +289,10 @@ async def open_root_actor(
|
||||||
for uw_addr in uw_reg_addrs
|
for uw_addr in uw_reg_addrs
|
||||||
]
|
]
|
||||||
|
|
||||||
loglevel: str = (
|
loglevel = (
|
||||||
loglevel
|
loglevel
|
||||||
or
|
or log._default_loglevel
|
||||||
log._default_loglevel
|
).upper()
|
||||||
)
|
|
||||||
loglevel: str = loglevel.upper()
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
debug_mode
|
debug_mode
|
||||||
|
|
@ -326,10 +323,7 @@ async def open_root_actor(
|
||||||
)
|
)
|
||||||
|
|
||||||
assert loglevel
|
assert loglevel
|
||||||
_log = log.get_console_log(
|
_log = log.get_console_log(loglevel)
|
||||||
level=loglevel,
|
|
||||||
name='tractor',
|
|
||||||
)
|
|
||||||
assert _log
|
assert _log
|
||||||
|
|
||||||
# TODO: factor this into `.devx._stackscope`!!
|
# TODO: factor this into `.devx._stackscope`!!
|
||||||
|
|
@ -386,13 +380,10 @@ async def open_root_actor(
|
||||||
addr,
|
addr,
|
||||||
)
|
)
|
||||||
|
|
||||||
tpt_bind_addrs: list[
|
trans_bind_addrs: list[UnwrappedAddress] = []
|
||||||
Address # `Address.get_random()` case
|
|
||||||
|UnwrappedAddress # registrar case `= uw_reg_addrs`
|
|
||||||
] = []
|
|
||||||
|
|
||||||
# ------ NON-REGISTRAR ------
|
# Create a new local root-actor instance which IS NOT THE
|
||||||
# create a new root-actor instance.
|
# REGISTRAR
|
||||||
if ponged_addrs:
|
if ponged_addrs:
|
||||||
if ensure_registry:
|
if ensure_registry:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
|
|
@ -419,21 +410,12 @@ async def open_root_actor(
|
||||||
# XXX INSTEAD, bind random addrs using the same tpt
|
# XXX INSTEAD, bind random addrs using the same tpt
|
||||||
# proto.
|
# proto.
|
||||||
for addr in ponged_addrs:
|
for addr in ponged_addrs:
|
||||||
tpt_bind_addrs.append(
|
trans_bind_addrs.append(
|
||||||
# XXX, these are `Address` NOT `UnwrappedAddress`.
|
|
||||||
#
|
|
||||||
# NOTE, in the case of posix/berkley socket
|
|
||||||
# protos we allocate port=0 such that the system
|
|
||||||
# allocates a random value at bind time; this
|
|
||||||
# happens in the `.ipc.*` stack's backend.
|
|
||||||
addr.get_random(
|
addr.get_random(
|
||||||
bindspace=addr.bindspace,
|
bindspace=addr.bindspace,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# ------ REGISTRAR ------
|
|
||||||
# create a new "registry providing" root-actor instance.
|
|
||||||
#
|
|
||||||
# Start this local actor as the "registrar", aka a regular
|
# Start this local actor as the "registrar", aka a regular
|
||||||
# actor who manages the local registry of "mailboxes" of
|
# actor who manages the local registry of "mailboxes" of
|
||||||
# other process-tree-local sub-actors.
|
# other process-tree-local sub-actors.
|
||||||
|
|
@ -442,7 +424,7 @@ async def open_root_actor(
|
||||||
# following init steps are taken:
|
# following init steps are taken:
|
||||||
# - the tranport layer server is bound to each addr
|
# - the tranport layer server is bound to each addr
|
||||||
# pair defined in provided registry_addrs, or the default.
|
# pair defined in provided registry_addrs, or the default.
|
||||||
tpt_bind_addrs = uw_reg_addrs
|
trans_bind_addrs = uw_reg_addrs
|
||||||
|
|
||||||
# - it is normally desirable for any registrar to stay up
|
# - it is normally desirable for any registrar to stay up
|
||||||
# indefinitely until either all registered (child/sub)
|
# indefinitely until either all registered (child/sub)
|
||||||
|
|
@ -462,10 +444,20 @@ async def open_root_actor(
|
||||||
enable_modules=enable_modules,
|
enable_modules=enable_modules,
|
||||||
)
|
)
|
||||||
# XXX, in case the root actor runtime was actually run from
|
# XXX, in case the root actor runtime was actually run from
|
||||||
# `tractor.to_asyncio.run_as_asyncio_guest()` and NOT
|
# `tractor.to_asyncio.run_as_asyncio_guest()` and NOt
|
||||||
# `.trio.run()`.
|
# `.trio.run()`.
|
||||||
actor._infected_aio = _state._runtime_vars['_is_infected_aio']
|
actor._infected_aio = _state._runtime_vars['_is_infected_aio']
|
||||||
|
|
||||||
|
# NOTE, only set the loopback addr for the
|
||||||
|
# process-tree-global "root" mailbox since all sub-actors
|
||||||
|
# should be able to speak to their root actor over that
|
||||||
|
# channel.
|
||||||
|
raddrs: list[Address] = _state._runtime_vars['_root_addrs']
|
||||||
|
raddrs.extend(trans_bind_addrs)
|
||||||
|
# TODO, remove once we have also removed all usage;
|
||||||
|
# eventually all (root-)registry apis should expect > 1 addr.
|
||||||
|
_state._runtime_vars['_root_mailbox'] = raddrs[0]
|
||||||
|
|
||||||
# Start up main task set via core actor-runtime nurseries.
|
# Start up main task set via core actor-runtime nurseries.
|
||||||
try:
|
try:
|
||||||
# assign process-local actor
|
# assign process-local actor
|
||||||
|
|
@ -502,39 +494,14 @@ async def open_root_actor(
|
||||||
# "actor runtime" primitives are SC-compat and thus all
|
# "actor runtime" primitives are SC-compat and thus all
|
||||||
# transitively spawned actors/processes must be as
|
# transitively spawned actors/processes must be as
|
||||||
# well.
|
# well.
|
||||||
accept_addrs: list[UnwrappedAddress]
|
await root_tn.start(
|
||||||
reg_addrs: list[UnwrappedAddress]
|
|
||||||
(
|
|
||||||
accept_addrs,
|
|
||||||
reg_addrs,
|
|
||||||
) = await root_tn.start(
|
|
||||||
partial(
|
partial(
|
||||||
_runtime.async_main,
|
_runtime.async_main,
|
||||||
actor,
|
actor,
|
||||||
accept_addrs=tpt_bind_addrs,
|
accept_addrs=trans_bind_addrs,
|
||||||
parent_addr=None
|
parent_addr=None
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# NOTE, only set a local-host addr (i.e. like
|
|
||||||
# `lo`-loopback for TCP) for the process-tree-global
|
|
||||||
# "root"-process (its tree-wide "mailbox") since all
|
|
||||||
# sub-actors should be able to speak to their root
|
|
||||||
# actor over that channel.
|
|
||||||
#
|
|
||||||
# ?TODO, per-OS non-network-proto alt options?
|
|
||||||
# -[ ] on linux we should be able to always use UDS?
|
|
||||||
#
|
|
||||||
raddrs: list[UnwrappedAddress] = _state._runtime_vars['_root_addrs']
|
|
||||||
raddrs.extend(
|
|
||||||
accept_addrs,
|
|
||||||
)
|
|
||||||
# TODO, remove once we have also removed all usage;
|
|
||||||
# eventually all (root-)registry apis should expect > 1 addr.
|
|
||||||
_state._runtime_vars['_root_mailbox'] = raddrs[0]
|
|
||||||
# if 'chart' in actor.aid.name:
|
|
||||||
# from tractor.devx import mk_pdb
|
|
||||||
# mk_pdb().set_trace()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield actor
|
yield actor
|
||||||
except (
|
except (
|
||||||
|
|
@ -616,13 +583,6 @@ async def open_root_actor(
|
||||||
):
|
):
|
||||||
_state._runtime_vars['_debug_mode'] = False
|
_state._runtime_vars['_debug_mode'] = False
|
||||||
|
|
||||||
# !XXX, clear ALL prior contact info state, this is MEGA
|
|
||||||
# important if you are opening the runtime multiple times
|
|
||||||
# from the same parent process (like in our test
|
|
||||||
# harness)!
|
|
||||||
_state._runtime_vars['_root_addrs'].clear()
|
|
||||||
_state._runtime_vars['_root_mailbox'] = None
|
|
||||||
|
|
||||||
_state._current_actor = None
|
_state._current_actor = None
|
||||||
_state._last_actor_terminated = actor
|
_state._last_actor_terminated = actor
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -284,15 +284,6 @@ async def _errors_relayed_via_ipc(
|
||||||
try:
|
try:
|
||||||
yield # run RPC invoke body
|
yield # run RPC invoke body
|
||||||
|
|
||||||
# NOTE, never REPL any pseudo-expected tpt-disconnect.
|
|
||||||
except TransportClosed as err:
|
|
||||||
rpc_err = err
|
|
||||||
log.warning(
|
|
||||||
f'Tpt disconnect during remote-exc relay due to,\n'
|
|
||||||
f'{err!r}\n'
|
|
||||||
)
|
|
||||||
raise err
|
|
||||||
|
|
||||||
# box and ship RPC errors for wire-transit via
|
# box and ship RPC errors for wire-transit via
|
||||||
# the task's requesting parent IPC-channel.
|
# the task's requesting parent IPC-channel.
|
||||||
except (
|
except (
|
||||||
|
|
@ -336,15 +327,10 @@ async def _errors_relayed_via_ipc(
|
||||||
# recovery logic - the only case is some kind of
|
# recovery logic - the only case is some kind of
|
||||||
# strange bug in our transport layer itself? Going
|
# strange bug in our transport layer itself? Going
|
||||||
# to keep this open ended for now.
|
# to keep this open ended for now.
|
||||||
|
log.debug(
|
||||||
if _state.debug_mode():
|
'RPC task crashed, attempting to enter debugger\n'
|
||||||
log.exception(
|
f'|_{ctx}'
|
||||||
f'RPC task crashed!\n'
|
|
||||||
f'Attempting to enter debugger\n'
|
|
||||||
f'\n'
|
|
||||||
f'{ctx}'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
entered_debug = await debug._maybe_enter_pm(
|
entered_debug = await debug._maybe_enter_pm(
|
||||||
err,
|
err,
|
||||||
api_frame=inspect.currentframe(),
|
api_frame=inspect.currentframe(),
|
||||||
|
|
@ -433,7 +419,7 @@ async def _errors_relayed_via_ipc(
|
||||||
# cancel scope will not have been inserted yet
|
# cancel scope will not have been inserted yet
|
||||||
if is_rpc:
|
if is_rpc:
|
||||||
log.warning(
|
log.warning(
|
||||||
'RPC task likely crashed or cancelled before start?\n'
|
'RPC task likely errored or cancelled before start?\n'
|
||||||
f'|_{ctx._task}\n'
|
f'|_{ctx._task}\n'
|
||||||
f' >> {ctx.repr_rpc}\n'
|
f' >> {ctx.repr_rpc}\n'
|
||||||
)
|
)
|
||||||
|
|
@ -876,9 +862,9 @@ async def _invoke(
|
||||||
)
|
)
|
||||||
|
|
||||||
logmeth(
|
logmeth(
|
||||||
f'{message}'
|
f'{message}\n'
|
||||||
f'\n'
|
f'\n'
|
||||||
f'{descr_str}'
|
f'{descr_str}\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -914,11 +900,6 @@ async def try_ship_error_to_remote(
|
||||||
|
|
||||||
# XXX NOTE XXX in SC terms this is one of the worst things
|
# XXX NOTE XXX in SC terms this is one of the worst things
|
||||||
# that can happen and provides for a 2-general's dilemma..
|
# that can happen and provides for a 2-general's dilemma..
|
||||||
#
|
|
||||||
# FURHTER, we should never really have to handle these
|
|
||||||
# lowlevel excs from `trio` since the `Channel.send()` layers
|
|
||||||
# downward should be mostly wrapping such cases in a
|
|
||||||
# tpt-closed; the `.critical()` usage is warranted.
|
|
||||||
except (
|
except (
|
||||||
trio.ClosedResourceError,
|
trio.ClosedResourceError,
|
||||||
trio.BrokenResourceError,
|
trio.BrokenResourceError,
|
||||||
|
|
|
||||||
|
|
@ -147,8 +147,6 @@ def get_mod_nsps2fps(mod_ns_paths: list[str]) -> dict[str, str]:
|
||||||
return nsp2fp
|
return nsp2fp
|
||||||
|
|
||||||
|
|
||||||
_bp = False
|
|
||||||
|
|
||||||
class Actor:
|
class Actor:
|
||||||
'''
|
'''
|
||||||
The fundamental "runtime" concurrency primitive.
|
The fundamental "runtime" concurrency primitive.
|
||||||
|
|
@ -183,14 +181,6 @@ class Actor:
|
||||||
def is_registrar(self) -> bool:
|
def is_registrar(self) -> bool:
|
||||||
return self.is_arbiter
|
return self.is_arbiter
|
||||||
|
|
||||||
@property
|
|
||||||
def is_root(self) -> bool:
|
|
||||||
'''
|
|
||||||
This actor is the parent most in the tree?
|
|
||||||
|
|
||||||
'''
|
|
||||||
return _state.is_root_process()
|
|
||||||
|
|
||||||
msg_buffer_size: int = 2**6
|
msg_buffer_size: int = 2**6
|
||||||
|
|
||||||
# nursery placeholders filled in by `async_main()`,
|
# nursery placeholders filled in by `async_main()`,
|
||||||
|
|
@ -282,9 +272,7 @@ class Actor:
|
||||||
stacklevel=2,
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
|
|
||||||
registry_addrs: list[Address] = [
|
registry_addrs: list[Address] = [wrap_address(arbiter_addr)]
|
||||||
wrap_address(arbiter_addr)
|
|
||||||
]
|
|
||||||
|
|
||||||
# marked by the process spawning backend at startup
|
# marked by the process spawning backend at startup
|
||||||
# will be None for the parent most process started manually
|
# will be None for the parent most process started manually
|
||||||
|
|
@ -971,21 +959,6 @@ class Actor:
|
||||||
|
|
||||||
rvs['_is_root'] = False # obvi XD
|
rvs['_is_root'] = False # obvi XD
|
||||||
|
|
||||||
# TODO, remove! left in just while protoing init fix!
|
|
||||||
# global _bp
|
|
||||||
# if (
|
|
||||||
# 'chart' in self.aid.name
|
|
||||||
# and
|
|
||||||
# isinstance(
|
|
||||||
# rvs['_root_addrs'][0],
|
|
||||||
# dict,
|
|
||||||
# )
|
|
||||||
# and
|
|
||||||
# not _bp
|
|
||||||
# ):
|
|
||||||
# _bp = True
|
|
||||||
# breakpoint()
|
|
||||||
|
|
||||||
_state._runtime_vars.update(rvs)
|
_state._runtime_vars.update(rvs)
|
||||||
|
|
||||||
# `SpawnSpec.reg_addrs`
|
# `SpawnSpec.reg_addrs`
|
||||||
|
|
@ -1482,12 +1455,7 @@ async def async_main(
|
||||||
# be False when running as root actor and True when as
|
# be False when running as root actor and True when as
|
||||||
# a subactor.
|
# a subactor.
|
||||||
parent_addr: UnwrappedAddress|None = None,
|
parent_addr: UnwrappedAddress|None = None,
|
||||||
task_status: TaskStatus[
|
task_status: TaskStatus[None] = trio.TASK_STATUS_IGNORED,
|
||||||
tuple[
|
|
||||||
list[UnwrappedAddress], # accept_addrs
|
|
||||||
list[UnwrappedAddress], # reg_addrs
|
|
||||||
]
|
|
||||||
] = trio.TASK_STATUS_IGNORED,
|
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
|
@ -1666,7 +1634,6 @@ async def async_main(
|
||||||
# if addresses point to the same actor..
|
# if addresses point to the same actor..
|
||||||
# So we need a way to detect that? maybe iterate
|
# So we need a way to detect that? maybe iterate
|
||||||
# only on unique actor uids?
|
# only on unique actor uids?
|
||||||
addr: UnwrappedAddress
|
|
||||||
for addr in actor.reg_addrs:
|
for addr in actor.reg_addrs:
|
||||||
try:
|
try:
|
||||||
waddr = wrap_address(addr)
|
waddr = wrap_address(addr)
|
||||||
|
|
@ -1675,9 +1642,7 @@ async def async_main(
|
||||||
await debug.pause()
|
await debug.pause()
|
||||||
|
|
||||||
# !TODO, get rid of the local-portal crap XD
|
# !TODO, get rid of the local-portal crap XD
|
||||||
reg_portal: Portal
|
|
||||||
async with get_registry(addr) as reg_portal:
|
async with get_registry(addr) as reg_portal:
|
||||||
accept_addr: UnwrappedAddress
|
|
||||||
for accept_addr in accept_addrs:
|
for accept_addr in accept_addrs:
|
||||||
accept_addr = wrap_address(accept_addr)
|
accept_addr = wrap_address(accept_addr)
|
||||||
|
|
||||||
|
|
@ -1693,12 +1658,8 @@ async def async_main(
|
||||||
|
|
||||||
is_registered: bool = True
|
is_registered: bool = True
|
||||||
|
|
||||||
# init steps complete, deliver IPC-server and
|
# init steps complete
|
||||||
# registrar addrs back to caller.
|
task_status.started()
|
||||||
task_status.started((
|
|
||||||
accept_addrs,
|
|
||||||
actor.reg_addrs,
|
|
||||||
))
|
|
||||||
|
|
||||||
# Begin handling our new connection back to our
|
# Begin handling our new connection back to our
|
||||||
# parent. This is done last since we don't want to
|
# parent. This is done last since we don't want to
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ from __future__ import annotations
|
||||||
from contextvars import (
|
from contextvars import (
|
||||||
ContextVar,
|
ContextVar,
|
||||||
)
|
)
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import (
|
from typing import (
|
||||||
Any,
|
Any,
|
||||||
|
|
@ -29,7 +30,6 @@ from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
import platformdirs
|
|
||||||
from trio.lowlevel import current_task
|
from trio.lowlevel import current_task
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -172,56 +172,23 @@ def current_ipc_ctx(
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
# std ODE (mutable) app state location
|
||||||
|
_rtdir: Path = Path(os.environ['XDG_RUNTIME_DIR'])
|
||||||
|
|
||||||
|
|
||||||
def get_rt_dir(
|
def get_rt_dir(
|
||||||
subdir: str|Path|None = None,
|
subdir: str = 'tractor'
|
||||||
appname: str = 'tractor',
|
|
||||||
) -> Path:
|
) -> Path:
|
||||||
'''
|
'''
|
||||||
Return the user "runtime dir", the file-sys location where most
|
Return the user "runtime dir" where most userspace apps stick
|
||||||
userspace apps stick their IPC and cache related system
|
their IPC and cache related system util-files; we take hold
|
||||||
util-files.
|
of a `'XDG_RUNTIME_DIR'/tractor/` subdir by default.
|
||||||
|
|
||||||
On linux we use a `${XDG_RUNTIME_DIR}/tractor/` subdir by
|
|
||||||
default, but equivalents are mapped for each platform using
|
|
||||||
the lovely `platformdirs` lib.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
rt_dir: Path = Path(
|
rtdir: Path = _rtdir / subdir
|
||||||
platformdirs.user_runtime_dir(
|
if not rtdir.is_dir():
|
||||||
appname=appname,
|
rtdir.mkdir()
|
||||||
),
|
return rtdir
|
||||||
)
|
|
||||||
|
|
||||||
# Normalize and validate that `subdir` is a relative path
|
|
||||||
# without any parent-directory ("..") components, to prevent
|
|
||||||
# escaping the runtime directory.
|
|
||||||
if subdir:
|
|
||||||
subdir_path = (
|
|
||||||
subdir
|
|
||||||
if isinstance(subdir, Path)
|
|
||||||
else Path(subdir)
|
|
||||||
)
|
|
||||||
if subdir_path.is_absolute():
|
|
||||||
raise ValueError(
|
|
||||||
f'`subdir` must be a relative path!\n'
|
|
||||||
f'{subdir!r}\n'
|
|
||||||
)
|
|
||||||
if any(part == '..' for part in subdir_path.parts):
|
|
||||||
raise ValueError(
|
|
||||||
"`subdir` must not contain '..' components!\n"
|
|
||||||
f'{subdir!r}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
rt_dir: Path = rt_dir / subdir_path
|
|
||||||
|
|
||||||
if not rt_dir.is_dir():
|
|
||||||
rt_dir.mkdir(
|
|
||||||
parents=True,
|
|
||||||
exist_ok=True, # avoid `FileExistsError` from conc calls
|
|
||||||
)
|
|
||||||
|
|
||||||
return rt_dir
|
|
||||||
|
|
||||||
|
|
||||||
def current_ipc_protos() -> list[str]:
|
def current_ipc_protos() -> list[str]:
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ import trio
|
||||||
from ._exceptions import (
|
from ._exceptions import (
|
||||||
ContextCancelled,
|
ContextCancelled,
|
||||||
RemoteActorError,
|
RemoteActorError,
|
||||||
TransportClosed,
|
|
||||||
)
|
)
|
||||||
from .log import get_logger
|
from .log import get_logger
|
||||||
from .trionics import (
|
from .trionics import (
|
||||||
|
|
@ -60,7 +59,7 @@ if TYPE_CHECKING:
|
||||||
from .ipc import Channel
|
from .ipc import Channel
|
||||||
|
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# TODO: the list
|
# TODO: the list
|
||||||
|
|
@ -410,8 +409,10 @@ class MsgStream(trio.abc.Channel):
|
||||||
# it).
|
# it).
|
||||||
with trio.CancelScope(shield=True):
|
with trio.CancelScope(shield=True):
|
||||||
await self._ctx.send_stop()
|
await self._ctx.send_stop()
|
||||||
|
|
||||||
except (
|
except (
|
||||||
TransportClosed,
|
trio.BrokenResourceError,
|
||||||
|
trio.ClosedResourceError
|
||||||
) as re:
|
) as re:
|
||||||
# the underlying channel may already have been pulled
|
# the underlying channel may already have been pulled
|
||||||
# in which case our stop message is meaningless since
|
# in which case our stop message is meaningless since
|
||||||
|
|
@ -592,8 +593,9 @@ class MsgStream(trio.abc.Channel):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
except (
|
except (
|
||||||
|
trio.ClosedResourceError,
|
||||||
|
trio.BrokenResourceError,
|
||||||
BrokenPipeError,
|
BrokenPipeError,
|
||||||
TransportClosed,
|
|
||||||
) as _trans_err:
|
) as _trans_err:
|
||||||
trans_err = _trans_err
|
trans_err = _trans_err
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ if TYPE_CHECKING:
|
||||||
from .ipc import IPCServer
|
from .ipc import IPCServer
|
||||||
|
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ActorNursery:
|
class ActorNursery:
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ from tractor.msg import (
|
||||||
import wrapt
|
import wrapt
|
||||||
|
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
# TODO: yeah, i don't love this and we should prolly just
|
# TODO: yeah, i don't love this and we should prolly just
|
||||||
# write a decorator that actually keeps a stupid ref to the func
|
# write a decorator that actually keeps a stupid ref to the func
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ from tractor import (
|
||||||
)
|
)
|
||||||
from tractor.devx import debug
|
from tractor.devx import debug
|
||||||
|
|
||||||
log = logmod.get_logger()
|
log = logmod.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ from ._sigint import (
|
||||||
_ctlc_ignore_header as _ctlc_ignore_header
|
_ctlc_ignore_header as _ctlc_ignore_header
|
||||||
)
|
)
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
# ----------------
|
# ----------------
|
||||||
# XXX PKG TODO XXX
|
# XXX PKG TODO XXX
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ _crash_msg: str = (
|
||||||
'Opening a pdb REPL in crashed actor'
|
'Opening a pdb REPL in crashed actor'
|
||||||
)
|
)
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__package__)
|
||||||
|
|
||||||
|
|
||||||
class BoxedMaybeException(Struct):
|
class BoxedMaybeException(Struct):
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ cancellation during REPL interaction.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import platform
|
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
@ -48,9 +47,8 @@ if TYPE_CHECKING:
|
||||||
Actor,
|
Actor,
|
||||||
)
|
)
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
_is_macos: bool = platform.system() == 'Darwin'
|
|
||||||
_ctlc_ignore_header: str = (
|
_ctlc_ignore_header: str = (
|
||||||
'Ignoring SIGINT while debug REPL in use'
|
'Ignoring SIGINT while debug REPL in use'
|
||||||
)
|
)
|
||||||
|
|
@ -302,11 +300,6 @@ def sigint_shield(
|
||||||
# XXX: yah, mega hack, but how else do we catch this madness XD
|
# XXX: yah, mega hack, but how else do we catch this madness XD
|
||||||
if (
|
if (
|
||||||
repl.shname == 'xonsh'
|
repl.shname == 'xonsh'
|
||||||
or (
|
|
||||||
repl.shname == 'bash'
|
|
||||||
and
|
|
||||||
_is_macos
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
flush_status += (
|
flush_status += (
|
||||||
'-> ALSO re-flushing due to `xonsh`..\n'
|
'-> ALSO re-flushing due to `xonsh`..\n'
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ from ._sigint import (
|
||||||
_ctlc_ignore_header as _ctlc_ignore_header
|
_ctlc_ignore_header as _ctlc_ignore_header
|
||||||
)
|
)
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__package__)
|
||||||
|
|
||||||
|
|
||||||
async def maybe_wait_for_debugger(
|
async def maybe_wait_for_debugger(
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ if TYPE_CHECKING:
|
||||||
# from ._post_mortem import BoxedMaybeException
|
# from ._post_mortem import BoxedMaybeException
|
||||||
from ._repl import PdbREPL
|
from ._repl import PdbREPL
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__package__)
|
||||||
|
|
||||||
_pause_msg: str = 'Opening a pdb REPL in paused actor'
|
_pause_msg: str = 'Opening a pdb REPL in paused actor'
|
||||||
_repl_fail_msg: str|None = (
|
_repl_fail_msg: str|None = (
|
||||||
|
|
@ -628,7 +628,7 @@ def _set_trace(
|
||||||
log.pdb(
|
log.pdb(
|
||||||
f'{_pause_msg}\n'
|
f'{_pause_msg}\n'
|
||||||
f'>(\n'
|
f'>(\n'
|
||||||
f'|_{actor.aid.uid}\n'
|
f'|_{actor.uid}\n'
|
||||||
f' |_{task}\n' # @ {actor.uid}\n'
|
f' |_{task}\n' # @ {actor.uid}\n'
|
||||||
# f'|_{task}\n'
|
# f'|_{task}\n'
|
||||||
# ^-TODO-^ more compact pformating?
|
# ^-TODO-^ more compact pformating?
|
||||||
|
|
@ -1257,26 +1257,3 @@ async def breakpoint(
|
||||||
api_frame=inspect.currentframe(),
|
api_frame=inspect.currentframe(),
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def maybe_pause_bp():
|
|
||||||
'''
|
|
||||||
Internal (ONLY for now) `breakpoint()`-er fn which only tries to
|
|
||||||
use the multi-actor `.pause()` API when the current actor is the
|
|
||||||
root.
|
|
||||||
|
|
||||||
?! BUT WHY !?
|
|
||||||
-------
|
|
||||||
|
|
||||||
This is useful when debugging cases where the tpt layer breaks
|
|
||||||
(or is intentionally broken, say during resiliency testing) in
|
|
||||||
the case where a child can no longer contact the root process to
|
|
||||||
acquire the process-tree-singleton TTY lock.
|
|
||||||
|
|
||||||
'''
|
|
||||||
import tractor
|
|
||||||
actor = tractor.current_actor()
|
|
||||||
if actor.aid.name == 'root':
|
|
||||||
await tractor.pause(shield=True)
|
|
||||||
else:
|
|
||||||
tractor.devx.mk_pdb().set_trace()
|
|
||||||
|
|
|
||||||
|
|
@ -81,7 +81,7 @@ if TYPE_CHECKING:
|
||||||
BoxedMaybeException,
|
BoxedMaybeException,
|
||||||
)
|
)
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class LockStatus(
|
class LockStatus(
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ if TYPE_CHECKING:
|
||||||
from ._transport import MsgTransport
|
from ._transport import MsgTransport
|
||||||
|
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
_is_windows = platform.system() == 'Windows'
|
_is_windows = platform.system() == 'Windows'
|
||||||
|
|
||||||
|
|
@ -307,12 +307,7 @@ class Channel:
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Send a coded msg-blob over the underlying IPC transport.
|
Send a coded msg-blob over the transport.
|
||||||
|
|
||||||
This fn raises `TransportClosed` on comms failures and is
|
|
||||||
normally handled by higher level runtime machinery for the
|
|
||||||
expected-graceful cases, normally ephemercal
|
|
||||||
(re/dis)connects.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
__tracebackhide__: bool = hide_tb
|
__tracebackhide__: bool = hide_tb
|
||||||
|
|
@ -339,10 +334,9 @@ class Channel:
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise err
|
raise err
|
||||||
case TransportClosed():
|
case TransportClosed():
|
||||||
src_exc_str: str = err.repr_src_exc()
|
|
||||||
log.transport(
|
log.transport(
|
||||||
f'Transport stream closed due to,\n'
|
f'Transport stream closed due to\n'
|
||||||
f'{src_exc_str}'
|
f'{err.repr_src_exc()}\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
case _:
|
case _:
|
||||||
|
|
@ -351,11 +345,6 @@ class Channel:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def recv(self) -> Any:
|
async def recv(self) -> Any:
|
||||||
'''
|
|
||||||
Receive the latest (queued) msg-blob from the underlying IPC
|
|
||||||
transport.
|
|
||||||
|
|
||||||
'''
|
|
||||||
assert self._transport
|
assert self._transport
|
||||||
return await self._transport.recv()
|
return await self._transport.recv()
|
||||||
|
|
||||||
|
|
@ -429,18 +418,16 @@ class Channel:
|
||||||
self
|
self
|
||||||
) -> AsyncGenerator[Any, None]:
|
) -> AsyncGenerator[Any, None]:
|
||||||
'''
|
'''
|
||||||
Yield `MsgType` IPC msgs decoded and deliverd from an
|
Yield `MsgType` IPC msgs decoded and deliverd from
|
||||||
underlying `MsgTransport` protocol.
|
an underlying `MsgTransport` protocol.
|
||||||
|
|
||||||
This is a streaming routine alo implemented as an
|
This is a streaming routine alo implemented as an async-gen
|
||||||
async-generator func (same a `MsgTransport._iter_pkts()`)
|
func (same a `MsgTransport._iter_pkts()`) gets allocated by
|
||||||
gets allocated by a `.__call__()` inside `.__init__()` where
|
a `.__call__()` inside `.__init__()` where it is assigned to
|
||||||
it is assigned to the `._aiter_msgs` attr.
|
the `._aiter_msgs` attr.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if not self._transport:
|
assert self._transport
|
||||||
raise RuntimeError('No IPC transport initialized!?')
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
async for msg in self._transport:
|
async for msg in self._transport:
|
||||||
|
|
@ -475,15 +462,7 @@ class Channel:
|
||||||
# continue
|
# continue
|
||||||
|
|
||||||
def connected(self) -> bool:
|
def connected(self) -> bool:
|
||||||
'''
|
return self._transport.connected() if self._transport else False
|
||||||
Predicate whether underlying IPC tpt is connected.
|
|
||||||
|
|
||||||
'''
|
|
||||||
return (
|
|
||||||
self._transport.connected()
|
|
||||||
if self._transport
|
|
||||||
else False
|
|
||||||
)
|
|
||||||
|
|
||||||
async def _do_handshake(
|
async def _do_handshake(
|
||||||
self,
|
self,
|
||||||
|
|
@ -514,11 +493,8 @@ async def _connect_chan(
|
||||||
addr: UnwrappedAddress
|
addr: UnwrappedAddress
|
||||||
) -> typing.AsyncGenerator[Channel, None]:
|
) -> typing.AsyncGenerator[Channel, None]:
|
||||||
'''
|
'''
|
||||||
Create and connect a `Channel` to the provided `addr`, disconnect
|
Create and connect a channel with disconnect on context manager
|
||||||
it on cm exit.
|
teardown.
|
||||||
|
|
||||||
NOTE, this is a lowlevel, normally internal-only iface. You
|
|
||||||
should likely use `.open_portal()` instead.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
chan = await Channel.from_addr(addr)
|
chan = await Channel.from_addr(addr)
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ if TYPE_CHECKING:
|
||||||
from .._supervise import ActorNursery
|
from .._supervise import ActorNursery
|
||||||
|
|
||||||
|
|
||||||
log = log.get_logger()
|
log = log.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def maybe_wait_on_canced_subs(
|
async def maybe_wait_on_canced_subs(
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ considered optional within the context of this runtime-library.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import hashlib
|
|
||||||
from multiprocessing import shared_memory as shm
|
from multiprocessing import shared_memory as shm
|
||||||
from multiprocessing.shared_memory import (
|
from multiprocessing.shared_memory import (
|
||||||
# SharedMemory,
|
# SharedMemory,
|
||||||
|
|
@ -60,7 +59,7 @@ except ImportError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
SharedMemory = disable_mantracker()
|
SharedMemory = disable_mantracker()
|
||||||
|
|
@ -107,12 +106,11 @@ class NDToken(Struct, frozen=True):
|
||||||
This type is msg safe.
|
This type is msg safe.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
shm_name: str # actual OS-level name (may be shortened on macOS)
|
shm_name: str # this servers as a "key" value
|
||||||
shm_first_index_name: str
|
shm_first_index_name: str
|
||||||
shm_last_index_name: str
|
shm_last_index_name: str
|
||||||
dtype_descr: tuple
|
dtype_descr: tuple
|
||||||
size: int # in struct-array index / row terms
|
size: int # in struct-array index / row terms
|
||||||
key: str|None = None # original descriptive key (for lookup)
|
|
||||||
|
|
||||||
# TODO: use nptyping here on dtypes
|
# TODO: use nptyping here on dtypes
|
||||||
@property
|
@property
|
||||||
|
|
@ -126,41 +124,6 @@ class NDToken(Struct, frozen=True):
|
||||||
def as_msg(self):
|
def as_msg(self):
|
||||||
return to_builtins(self)
|
return to_builtins(self)
|
||||||
|
|
||||||
def __eq__(self, other) -> bool:
|
|
||||||
'''
|
|
||||||
Compare tokens based on shm names and dtype,
|
|
||||||
ignoring the `key` field.
|
|
||||||
|
|
||||||
The `key` field is only used for lookups,
|
|
||||||
not for token identity.
|
|
||||||
|
|
||||||
'''
|
|
||||||
if not isinstance(other, NDToken):
|
|
||||||
return False
|
|
||||||
return (
|
|
||||||
self.shm_name == other.shm_name
|
|
||||||
and self.shm_first_index_name
|
|
||||||
== other.shm_first_index_name
|
|
||||||
and self.shm_last_index_name
|
|
||||||
== other.shm_last_index_name
|
|
||||||
and self.dtype_descr == other.dtype_descr
|
|
||||||
and self.size == other.size
|
|
||||||
)
|
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
|
||||||
'''
|
|
||||||
Hash based on the same fields used
|
|
||||||
in `.__eq__()`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
return hash((
|
|
||||||
self.shm_name,
|
|
||||||
self.shm_first_index_name,
|
|
||||||
self.shm_last_index_name,
|
|
||||||
self.dtype_descr,
|
|
||||||
self.size,
|
|
||||||
))
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_msg(cls, msg: dict) -> NDToken:
|
def from_msg(cls, msg: dict) -> NDToken:
|
||||||
if isinstance(msg, NDToken):
|
if isinstance(msg, NDToken):
|
||||||
|
|
@ -197,50 +160,6 @@ def get_shm_token(key: str) -> NDToken | None:
|
||||||
return _known_tokens.get(key)
|
return _known_tokens.get(key)
|
||||||
|
|
||||||
|
|
||||||
def _shorten_key_for_macos(
|
|
||||||
key: str,
|
|
||||||
prefix: str = '',
|
|
||||||
suffix: str = '',
|
|
||||||
) -> str:
|
|
||||||
'''
|
|
||||||
MacOS has a (hillarious) 31 character limit for POSIX shared
|
|
||||||
memory names. Hash long keys to fit within this limit while
|
|
||||||
maintaining uniqueness.
|
|
||||||
|
|
||||||
'''
|
|
||||||
# macOS shm_open() has a 31 char limit (PSHMNAMLEN)
|
|
||||||
# format: /t_<hash16> = 19 chars, well under limit
|
|
||||||
max_len: int = 31
|
|
||||||
if len(key) <= max_len:
|
|
||||||
return key
|
|
||||||
|
|
||||||
_hash: str = hashlib.sha256(
|
|
||||||
key.encode()
|
|
||||||
).hexdigest()
|
|
||||||
|
|
||||||
hash_len: int = (
|
|
||||||
(max_len - 1)
|
|
||||||
- len(prefix)
|
|
||||||
- len(suffix)
|
|
||||||
)
|
|
||||||
key_hash: str = _hash[:hash_len]
|
|
||||||
short_key = (
|
|
||||||
prefix
|
|
||||||
+
|
|
||||||
f'{key_hash}'
|
|
||||||
+
|
|
||||||
suffix
|
|
||||||
)
|
|
||||||
|
|
||||||
log.debug(
|
|
||||||
f'Shortened shm key for macOS:\n'
|
|
||||||
f' original: {key!r} ({len(key)!r} chars)\n'
|
|
||||||
f' shortened: {short_key!r}'
|
|
||||||
f' ({len(short_key)!r} chars)'
|
|
||||||
)
|
|
||||||
return short_key
|
|
||||||
|
|
||||||
|
|
||||||
def _make_token(
|
def _make_token(
|
||||||
key: str,
|
key: str,
|
||||||
size: int,
|
size: int,
|
||||||
|
|
@ -252,32 +171,12 @@ def _make_token(
|
||||||
to access a shared array.
|
to access a shared array.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# On macOS, shorten keys that exceed the
|
|
||||||
# 31 character limit
|
|
||||||
if platform.system() == 'Darwin':
|
|
||||||
shm_name = _shorten_key_for_macos(
|
|
||||||
key=key,
|
|
||||||
)
|
|
||||||
shm_first = _shorten_key_for_macos(
|
|
||||||
key=key,
|
|
||||||
suffix='_first',
|
|
||||||
)
|
|
||||||
shm_last = _shorten_key_for_macos(
|
|
||||||
key=key,
|
|
||||||
suffix='_last',
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
shm_name = key
|
|
||||||
shm_first = key + '_first'
|
|
||||||
shm_last = key + '_last'
|
|
||||||
|
|
||||||
return NDToken(
|
return NDToken(
|
||||||
shm_name=shm_name,
|
shm_name=key,
|
||||||
shm_first_index_name=shm_first,
|
shm_first_index_name=key + "_first",
|
||||||
shm_last_index_name=shm_last,
|
shm_last_index_name=key + "_last",
|
||||||
dtype_descr=tuple(np.dtype(dtype).descr),
|
dtype_descr=tuple(np.dtype(dtype).descr),
|
||||||
size=size,
|
size=size,
|
||||||
key=key, # store original key for lookup
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -532,17 +431,9 @@ class ShmArray:
|
||||||
|
|
||||||
def destroy(self) -> None:
|
def destroy(self) -> None:
|
||||||
if _USE_POSIX:
|
if _USE_POSIX:
|
||||||
# We manually unlink to bypass all the
|
# We manually unlink to bypass all the "resource tracker"
|
||||||
# "resource tracker" nonsense meant for
|
# nonsense meant for non-SC systems.
|
||||||
# non-SC systems.
|
shm_unlink(self._shm.name)
|
||||||
name = self._shm.name
|
|
||||||
try:
|
|
||||||
shm_unlink(name)
|
|
||||||
except FileNotFoundError:
|
|
||||||
# might be a teardown race here?
|
|
||||||
log.warning(
|
|
||||||
f'Shm for {name} already unlinked?'
|
|
||||||
)
|
|
||||||
|
|
||||||
self._first.destroy()
|
self._first.destroy()
|
||||||
self._last.destroy()
|
self._last.destroy()
|
||||||
|
|
@ -572,16 +463,8 @@ def open_shm_ndarray(
|
||||||
a = np.zeros(size, dtype=dtype)
|
a = np.zeros(size, dtype=dtype)
|
||||||
a['index'] = np.arange(len(a))
|
a['index'] = np.arange(len(a))
|
||||||
|
|
||||||
# Create token first to get the (possibly
|
|
||||||
# shortened) shm name
|
|
||||||
token = _make_token(
|
|
||||||
key=key,
|
|
||||||
size=size,
|
|
||||||
dtype=dtype,
|
|
||||||
)
|
|
||||||
|
|
||||||
shm = SharedMemory(
|
shm = SharedMemory(
|
||||||
name=token.shm_name,
|
name=key,
|
||||||
create=True,
|
create=True,
|
||||||
size=a.nbytes
|
size=a.nbytes
|
||||||
)
|
)
|
||||||
|
|
@ -593,6 +476,12 @@ def open_shm_ndarray(
|
||||||
array[:] = a[:]
|
array[:] = a[:]
|
||||||
array.setflags(write=int(not readonly))
|
array.setflags(write=int(not readonly))
|
||||||
|
|
||||||
|
token = _make_token(
|
||||||
|
key=key,
|
||||||
|
size=size,
|
||||||
|
dtype=dtype,
|
||||||
|
)
|
||||||
|
|
||||||
# create single entry arrays for storing an first and last indices
|
# create single entry arrays for storing an first and last indices
|
||||||
first = SharedInt(
|
first = SharedInt(
|
||||||
shm=SharedMemory(
|
shm=SharedMemory(
|
||||||
|
|
@ -665,23 +554,13 @@ def attach_shm_ndarray(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
token = NDToken.from_msg(token)
|
token = NDToken.from_msg(token)
|
||||||
# Use original key for _known_tokens lookup,
|
key = token.shm_name
|
||||||
# shm_name for OS calls
|
|
||||||
lookup_key = (
|
|
||||||
token.key if token.key
|
|
||||||
else token.shm_name
|
|
||||||
)
|
|
||||||
|
|
||||||
if lookup_key in _known_tokens:
|
if key in _known_tokens:
|
||||||
assert (
|
assert NDToken.from_msg(_known_tokens[key]) == token, "WTF"
|
||||||
NDToken.from_msg(
|
|
||||||
_known_tokens[lookup_key]
|
|
||||||
) == token
|
|
||||||
), 'WTF'
|
|
||||||
|
|
||||||
# XXX: ugh, looks like due to the ``shm_open()``
|
# XXX: ugh, looks like due to the ``shm_open()`` C api we can't
|
||||||
# C api we can't actually place files in a subdir,
|
# actually place files in a subdir, see discussion here:
|
||||||
# see discussion here:
|
|
||||||
# https://stackoverflow.com/a/11103289
|
# https://stackoverflow.com/a/11103289
|
||||||
|
|
||||||
# attach to array buffer and view as per dtype
|
# attach to array buffer and view as per dtype
|
||||||
|
|
@ -689,7 +568,7 @@ def attach_shm_ndarray(
|
||||||
for _ in range(3):
|
for _ in range(3):
|
||||||
try:
|
try:
|
||||||
shm = SharedMemory(
|
shm = SharedMemory(
|
||||||
name=token.shm_name,
|
name=key,
|
||||||
create=False,
|
create=False,
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
@ -735,10 +614,10 @@ def attach_shm_ndarray(
|
||||||
sha.array
|
sha.array
|
||||||
|
|
||||||
# Stash key -> token knowledge for future queries
|
# Stash key -> token knowledge for future queries
|
||||||
# via `maybe_open_shm_ndarray()` but only after
|
# via `maybe_opepn_shm_array()` but only after we know
|
||||||
# we know we can attach.
|
# we can attach.
|
||||||
if lookup_key not in _known_tokens:
|
if key not in _known_tokens:
|
||||||
_known_tokens[lookup_key] = token
|
_known_tokens[key] = token
|
||||||
|
|
||||||
# "close" attached shm on actor teardown
|
# "close" attached shm on actor teardown
|
||||||
tractor.current_actor().lifetime_stack.callback(sha.close)
|
tractor.current_actor().lifetime_stack.callback(sha.close)
|
||||||
|
|
@ -782,10 +661,7 @@ def maybe_open_shm_ndarray(
|
||||||
False, # not newly opened
|
False, # not newly opened
|
||||||
)
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
log.warning(
|
log.warning(f"Could not find {key} in shms cache")
|
||||||
f'Could not find key in shms cache,\n'
|
|
||||||
f'key: {key!r}\n'
|
|
||||||
)
|
|
||||||
if dtype:
|
if dtype:
|
||||||
token = _make_token(
|
token = _make_token(
|
||||||
key,
|
key,
|
||||||
|
|
@ -895,7 +771,6 @@ def open_shm_list(
|
||||||
size: int = int(2 ** 10),
|
size: int = int(2 ** 10),
|
||||||
dtype: float | int | bool | str | bytes | None = float,
|
dtype: float | int | bool | str | bytes | None = float,
|
||||||
readonly: bool = True,
|
readonly: bool = True,
|
||||||
prefix: str = 'shml_',
|
|
||||||
|
|
||||||
) -> ShmList:
|
) -> ShmList:
|
||||||
|
|
||||||
|
|
@ -909,12 +784,6 @@ def open_shm_list(
|
||||||
}[dtype]
|
}[dtype]
|
||||||
sequence = [default] * size
|
sequence = [default] * size
|
||||||
|
|
||||||
if platform.system() == 'Darwin':
|
|
||||||
key: str = _shorten_key_for_macos(
|
|
||||||
key=key,
|
|
||||||
prefix=prefix,
|
|
||||||
)
|
|
||||||
|
|
||||||
shml = ShmList(
|
shml = ShmList(
|
||||||
sequence=sequence,
|
sequence=sequence,
|
||||||
name=key,
|
name=key,
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ from tractor.ipc._transport import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TCPAddress(
|
class TCPAddress(
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ from tractor.msg import (
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from tractor._addr import Address
|
from tractor._addr import Address
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# (codec, transport)
|
# (codec, transport)
|
||||||
|
|
@ -154,6 +154,7 @@ class MsgTransport(Protocol):
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MsgpackTransport(MsgTransport):
|
class MsgpackTransport(MsgTransport):
|
||||||
|
|
||||||
# TODO: better naming for this?
|
# TODO: better naming for this?
|
||||||
|
|
@ -277,18 +278,14 @@ class MsgpackTransport(MsgTransport):
|
||||||
except trio.ClosedResourceError as cre:
|
except trio.ClosedResourceError as cre:
|
||||||
closure_err = cre
|
closure_err = cre
|
||||||
|
|
||||||
# await tractor.devx._trace.maybe_pause_bp()
|
|
||||||
|
|
||||||
raise TransportClosed(
|
raise TransportClosed(
|
||||||
message=(
|
message=(
|
||||||
f'{tpt_name} was already closed locally?'
|
f'{tpt_name} was already closed locally ?\n'
|
||||||
),
|
),
|
||||||
src_exc=closure_err,
|
src_exc=closure_err,
|
||||||
loglevel='error',
|
loglevel='error',
|
||||||
raise_on_report=(
|
raise_on_report=(
|
||||||
'another task closed this fd'
|
'another task closed this fd' in closure_err.args
|
||||||
in
|
|
||||||
closure_err.args
|
|
||||||
),
|
),
|
||||||
) from closure_err
|
) from closure_err
|
||||||
|
|
||||||
|
|
@ -438,11 +435,6 @@ class MsgpackTransport(MsgTransport):
|
||||||
trans_err = _re
|
trans_err = _re
|
||||||
tpt_name: str = f'{type(self).__name__!r}'
|
tpt_name: str = f'{type(self).__name__!r}'
|
||||||
|
|
||||||
trans_err_msg: str = trans_err.args[0]
|
|
||||||
by_whom: str = {
|
|
||||||
'another task closed this fd': 'locally',
|
|
||||||
'this socket was already closed': 'by peer',
|
|
||||||
}.get(trans_err_msg)
|
|
||||||
match trans_err:
|
match trans_err:
|
||||||
|
|
||||||
# XXX, specifc to UDS transport and its,
|
# XXX, specifc to UDS transport and its,
|
||||||
|
|
@ -454,42 +446,38 @@ class MsgpackTransport(MsgTransport):
|
||||||
case trio.BrokenResourceError() if (
|
case trio.BrokenResourceError() if (
|
||||||
'[Errno 32] Broken pipe'
|
'[Errno 32] Broken pipe'
|
||||||
in
|
in
|
||||||
trans_err_msg
|
trans_err.args[0]
|
||||||
):
|
):
|
||||||
tpt_closed = TransportClosed.from_src_exc(
|
tpt_closed = TransportClosed.from_src_exc(
|
||||||
message=(
|
message=(
|
||||||
f'{tpt_name} already closed by peer\n'
|
f'{tpt_name} already closed by peer\n'
|
||||||
),
|
),
|
||||||
body=f'{self}',
|
body=f'{self}\n',
|
||||||
src_exc=trans_err,
|
src_exc=trans_err,
|
||||||
raise_on_report=True,
|
raise_on_report=True,
|
||||||
loglevel='transport',
|
loglevel='transport',
|
||||||
)
|
)
|
||||||
raise tpt_closed from trans_err
|
raise tpt_closed from trans_err
|
||||||
|
|
||||||
# ??TODO??, what case in piker does this and HOW
|
# case trio.ClosedResourceError() if (
|
||||||
# CAN WE RE-PRODUCE IT?!?!?
|
# 'this socket was already closed'
|
||||||
case trio.ClosedResourceError() if (
|
# in
|
||||||
by_whom
|
# trans_err.args[0]
|
||||||
):
|
# ):
|
||||||
tpt_closed = TransportClosed.from_src_exc(
|
# tpt_closed = TransportClosed.from_src_exc(
|
||||||
message=(
|
# message=(
|
||||||
f'{tpt_name} was already closed {by_whom!r}?\n'
|
# f'{tpt_name} already closed by peer\n'
|
||||||
),
|
# ),
|
||||||
body=f'{self}',
|
# body=f'{self}\n',
|
||||||
src_exc=trans_err,
|
# src_exc=trans_err,
|
||||||
raise_on_report=True,
|
# raise_on_report=True,
|
||||||
loglevel='transport',
|
# loglevel='transport',
|
||||||
)
|
# )
|
||||||
|
# raise tpt_closed from trans_err
|
||||||
|
|
||||||
# await tractor.devx._trace.maybe_pause_bp()
|
# unless the disconnect condition falls under "a
|
||||||
raise tpt_closed from trans_err
|
# normal operation breakage" we usualy console warn
|
||||||
|
# about it.
|
||||||
# XXX, unless the disconnect condition falls
|
|
||||||
# under "a normal/expected operating breakage"
|
|
||||||
# (per the `trans_err_msg` guards in the cases
|
|
||||||
# above) we usualy console-error about it and
|
|
||||||
# raise-thru. about it.
|
|
||||||
case _:
|
case _:
|
||||||
log.exception(
|
log.exception(
|
||||||
f'{tpt_name} layer failed pre-send ??\n'
|
f'{tpt_name} layer failed pre-send ??\n'
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,12 @@ from contextlib import (
|
||||||
)
|
)
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
from socket import (
|
from socket import (
|
||||||
AF_UNIX,
|
AF_UNIX,
|
||||||
SOCK_STREAM,
|
SOCK_STREAM,
|
||||||
|
SO_PASSCRED,
|
||||||
|
SO_PEERCRED,
|
||||||
SOL_SOCKET,
|
SOL_SOCKET,
|
||||||
error as socket_error,
|
|
||||||
)
|
)
|
||||||
import struct
|
import struct
|
||||||
from typing import (
|
from typing import (
|
||||||
|
|
@ -53,7 +53,7 @@ from tractor.log import get_logger
|
||||||
from tractor.ipc._transport import (
|
from tractor.ipc._transport import (
|
||||||
MsgpackTransport,
|
MsgpackTransport,
|
||||||
)
|
)
|
||||||
from tractor._state import (
|
from .._state import (
|
||||||
get_rt_dir,
|
get_rt_dir,
|
||||||
current_actor,
|
current_actor,
|
||||||
is_root_process,
|
is_root_process,
|
||||||
|
|
@ -63,29 +63,7 @@ if TYPE_CHECKING:
|
||||||
from ._runtime import Actor
|
from ._runtime import Actor
|
||||||
|
|
||||||
|
|
||||||
# Platform-specific credential passing constants
|
log = get_logger(__name__)
|
||||||
# See: https://stackoverflow.com/a/7982749
|
|
||||||
if sys.platform == 'linux':
|
|
||||||
from socket import (
|
|
||||||
SO_PASSCRED,
|
|
||||||
SO_PEERCRED,
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# Other (Unix) platforms - though further testing is required and
|
|
||||||
# others may need additional special handling?
|
|
||||||
SO_PASSCRED = None
|
|
||||||
SO_PEERCRED = None
|
|
||||||
|
|
||||||
# NOTE, macOS uses `LOCAL_PEERCRED` instead of `SO_PEERCRED` and
|
|
||||||
# doesn't need `SO_PASSCRED` (credential passing is always enabled).
|
|
||||||
# See code in <sys/un.h>: `#define LOCAL_PEERCRED 0x001`
|
|
||||||
#
|
|
||||||
# XXX INSTEAD we use the (hopefully) more generic
|
|
||||||
# `get_peer_pid()` below for other OSes.
|
|
||||||
|
|
||||||
|
|
||||||
log = get_logger()
|
|
||||||
|
|
||||||
|
|
||||||
def unwrap_sockpath(
|
def unwrap_sockpath(
|
||||||
|
|
@ -187,15 +165,7 @@ class UDSAddress(
|
||||||
err_on_no_runtime=False,
|
err_on_no_runtime=False,
|
||||||
)
|
)
|
||||||
if actor:
|
if actor:
|
||||||
sockname: str = f'{actor.aid.name}@{pid}'
|
sockname: str = '::'.join(actor.uid) + f'@{pid}'
|
||||||
# XXX, orig version which broke both macOS (file-name
|
|
||||||
# length) and `multiaddrs` ('::' invalid separator).
|
|
||||||
# sockname: str = '::'.join(actor.uid) + f'@{pid}'
|
|
||||||
#
|
|
||||||
# ?^TODO, for `multiaddr`'s parser we can't use the `::`
|
|
||||||
# above^, SO maybe a `.` or something else here?
|
|
||||||
# sockname: str = '.'.join(actor.uid) + f'@{pid}'
|
|
||||||
# -[ ] CURRENTLY using `.` BREAKS TEST SUITE tho..
|
|
||||||
else:
|
else:
|
||||||
prefix: str = '<unknown-actor>'
|
prefix: str = '<unknown-actor>'
|
||||||
if is_root_process():
|
if is_root_process():
|
||||||
|
|
@ -318,12 +288,7 @@ def close_listener(
|
||||||
|
|
||||||
|
|
||||||
async def open_unix_socket_w_passcred(
|
async def open_unix_socket_w_passcred(
|
||||||
filename: (
|
filename: str|bytes|os.PathLike[str]|os.PathLike[bytes],
|
||||||
str
|
|
||||||
|bytes
|
|
||||||
|os.PathLike[str]
|
|
||||||
|os.PathLike[bytes]
|
|
||||||
),
|
|
||||||
) -> trio.SocketStream:
|
) -> trio.SocketStream:
|
||||||
'''
|
'''
|
||||||
Literally the exact same as `trio.open_unix_socket()` except we set the additiona
|
Literally the exact same as `trio.open_unix_socket()` except we set the additiona
|
||||||
|
|
@ -341,66 +306,21 @@ async def open_unix_socket_w_passcred(
|
||||||
# much more simplified logic vs tcp sockets - one socket type and only one
|
# much more simplified logic vs tcp sockets - one socket type and only one
|
||||||
# possible location to connect to
|
# possible location to connect to
|
||||||
sock = trio.socket.socket(AF_UNIX, SOCK_STREAM)
|
sock = trio.socket.socket(AF_UNIX, SOCK_STREAM)
|
||||||
|
|
||||||
# Only set SO_PASSCRED on Linux (not needed/available on macOS)
|
|
||||||
if SO_PASSCRED is not None:
|
|
||||||
sock.setsockopt(SOL_SOCKET, SO_PASSCRED, 1)
|
sock.setsockopt(SOL_SOCKET, SO_PASSCRED, 1)
|
||||||
|
|
||||||
with close_on_error(sock):
|
with close_on_error(sock):
|
||||||
await sock.connect(os.fspath(filename))
|
await sock.connect(os.fspath(filename))
|
||||||
|
|
||||||
return trio.SocketStream(sock)
|
return trio.SocketStream(sock)
|
||||||
|
|
||||||
|
|
||||||
def get_peer_pid(sock) -> int|None:
|
def get_peer_info(sock: trio.socket.socket) -> tuple[
|
||||||
'''
|
|
||||||
Gets the PID of the process connected to the other end of a Unix
|
|
||||||
domain socket on macOS, or `None` if that fails.
|
|
||||||
|
|
||||||
NOTE, should work on MacOS (and others?).
|
|
||||||
|
|
||||||
'''
|
|
||||||
# try to get the peer PID using a naive soln found from,
|
|
||||||
# https://stackoverflow.com/a/67971484
|
|
||||||
#
|
|
||||||
# NOTE, a more correct soln is likely needed here according to
|
|
||||||
# the complaints of `copilot` which led to digging into the
|
|
||||||
# underlying `go`lang issue linked from the above SO answer,
|
|
||||||
|
|
||||||
# XXX, darwin-xnu kernel srces defining these constants,
|
|
||||||
# - SOL_LOCAL
|
|
||||||
# |_https://github.com/apple/darwin-xnu/blob/main/bsd/sys/un.h#L85
|
|
||||||
# - LOCAL_PEERPID
|
|
||||||
# |_https://github.com/apple/darwin-xnu/blob/main/bsd/sys/un.h#L89
|
|
||||||
#
|
|
||||||
SOL_LOCAL: int = 0
|
|
||||||
LOCAL_PEERPID: int = 0x002
|
|
||||||
|
|
||||||
try:
|
|
||||||
pid: int = sock.getsockopt(
|
|
||||||
SOL_LOCAL,
|
|
||||||
LOCAL_PEERPID,
|
|
||||||
)
|
|
||||||
return pid
|
|
||||||
except socket_error as e:
|
|
||||||
log.exception(
|
|
||||||
f"Failed to get peer PID: {e}"
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_peer_info(
|
|
||||||
sock: trio.socket.socket,
|
|
||||||
) -> tuple[
|
|
||||||
int, # pid
|
int, # pid
|
||||||
int, # uid
|
int, # uid
|
||||||
int, # guid
|
int, # guid
|
||||||
]:
|
]:
|
||||||
'''
|
'''
|
||||||
Deliver the connecting peer's "credentials"-info as defined in
|
Deliver the connecting peer's "credentials"-info as defined in
|
||||||
a platform-specific way.
|
a very Linux specific way..
|
||||||
|
|
||||||
Linux-ONLY, uses SO_PEERCRED.
|
|
||||||
|
|
||||||
For more deats see,
|
For more deats see,
|
||||||
- `man accept`,
|
- `man accept`,
|
||||||
|
|
@ -413,11 +333,6 @@ def get_peer_info(
|
||||||
- https://stackoverflow.com/a/7982749
|
- https://stackoverflow.com/a/7982749
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if SO_PEERCRED is None:
|
|
||||||
raise RuntimeError(
|
|
||||||
f'Peer credential retrieval not supported on {sys.platform}!'
|
|
||||||
)
|
|
||||||
|
|
||||||
creds: bytes = sock.getsockopt(
|
creds: bytes = sock.getsockopt(
|
||||||
SOL_SOCKET,
|
SOL_SOCKET,
|
||||||
SO_PEERCRED,
|
SO_PEERCRED,
|
||||||
|
|
@ -521,38 +436,14 @@ class MsgpackUDSStream(MsgpackTransport):
|
||||||
match (peername, sockname):
|
match (peername, sockname):
|
||||||
case (str(), bytes()):
|
case (str(), bytes()):
|
||||||
sock_path: Path = Path(peername)
|
sock_path: Path = Path(peername)
|
||||||
|
|
||||||
case (bytes(), str()):
|
case (bytes(), str()):
|
||||||
sock_path: Path = Path(sockname)
|
sock_path: Path = Path(sockname)
|
||||||
|
|
||||||
case (str(), str()): # XXX, likely macOS
|
|
||||||
sock_path: Path = Path(peername)
|
|
||||||
|
|
||||||
case _:
|
|
||||||
raise TypeError(
|
|
||||||
f'Failed to match (peername, sockname) types?\n'
|
|
||||||
f'peername: {peername!r}\n'
|
|
||||||
f'sockname: {sockname!r}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
if sys.platform == 'linux':
|
|
||||||
(
|
(
|
||||||
peer_pid,
|
peer_pid,
|
||||||
_,
|
_,
|
||||||
_,
|
_,
|
||||||
) = get_peer_info(sock)
|
) = get_peer_info(sock)
|
||||||
|
|
||||||
# NOTE known to at least works on,
|
|
||||||
# - macos
|
|
||||||
else:
|
|
||||||
peer_pid: int|None = get_peer_pid(sock)
|
|
||||||
if peer_pid is None:
|
|
||||||
log.warning(
|
|
||||||
f'Unable to get peer PID?\n'
|
|
||||||
f'sock: {sock!r}\n'
|
|
||||||
f'peer_pid: {peer_pid!r}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
filedir, filename = unwrap_sockpath(sock_path)
|
filedir, filename = unwrap_sockpath(sock_path)
|
||||||
laddr = UDSAddress(
|
laddr = UDSAddress(
|
||||||
filedir=filedir,
|
filedir=filedir,
|
||||||
|
|
|
||||||
363
tractor/log.py
363
tractor/log.py
|
|
@ -14,23 +14,11 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
'''
|
"""
|
||||||
An enhanced logging subsys.
|
Log like a forester!
|
||||||
|
|
||||||
An extended logging layer using (for now) the stdlib's `logging`
|
"""
|
||||||
+ `colorlog` which embeds concurrency-primitive/runtime info into
|
|
||||||
records (headers) to help you better grok your distributed systems
|
|
||||||
built on `tractor`.
|
|
||||||
|
|
||||||
|
|
||||||
'''
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
from functools import partial
|
|
||||||
from inspect import (
|
|
||||||
FrameInfo,
|
|
||||||
getmodule,
|
|
||||||
stack,
|
|
||||||
)
|
|
||||||
import sys
|
import sys
|
||||||
import logging
|
import logging
|
||||||
from logging import (
|
from logging import (
|
||||||
|
|
@ -38,24 +26,20 @@ from logging import (
|
||||||
Logger,
|
Logger,
|
||||||
StreamHandler,
|
StreamHandler,
|
||||||
)
|
)
|
||||||
from types import ModuleType
|
|
||||||
import warnings
|
|
||||||
|
|
||||||
import colorlog # type: ignore
|
import colorlog # type: ignore
|
||||||
# ?TODO, some other (modern) alt libs?
|
|
||||||
# import coloredlogs
|
|
||||||
# import colored_traceback.auto # ?TODO, need better config?
|
|
||||||
import trio
|
import trio
|
||||||
|
|
||||||
from ._state import current_actor
|
from ._state import current_actor
|
||||||
|
|
||||||
|
|
||||||
|
_proj_name: str = 'tractor'
|
||||||
_default_loglevel: str = 'ERROR'
|
_default_loglevel: str = 'ERROR'
|
||||||
|
|
||||||
# Super sexy formatting thanks to ``colorlog``.
|
# Super sexy formatting thanks to ``colorlog``.
|
||||||
# (NOTE: we use the '{' format style)
|
# (NOTE: we use the '{' format style)
|
||||||
# Here, `thin_white` is just the layperson's gray.
|
# Here, `thin_white` is just the layperson's gray.
|
||||||
LOG_FORMAT: str = (
|
LOG_FORMAT = (
|
||||||
# "{bold_white}{log_color}{asctime}{reset}"
|
# "{bold_white}{log_color}{asctime}{reset}"
|
||||||
"{log_color}{asctime}{reset}"
|
"{log_color}{asctime}{reset}"
|
||||||
" {bold_white}{thin_white}({reset}"
|
" {bold_white}{thin_white}({reset}"
|
||||||
|
|
@ -67,7 +51,7 @@ LOG_FORMAT: str = (
|
||||||
" {reset}{bold_white}{thin_white}{message}"
|
" {reset}{bold_white}{thin_white}{message}"
|
||||||
)
|
)
|
||||||
|
|
||||||
DATE_FORMAT: str = '%b %d %H:%M:%S'
|
DATE_FORMAT = '%b %d %H:%M:%S'
|
||||||
|
|
||||||
# FYI, ERROR is 40
|
# FYI, ERROR is 40
|
||||||
# TODO: use a `bidict` to avoid the :155 check?
|
# TODO: use a `bidict` to avoid the :155 check?
|
||||||
|
|
@ -91,10 +75,7 @@ STD_PALETTE = {
|
||||||
'TRANSPORT': 'cyan',
|
'TRANSPORT': 'cyan',
|
||||||
}
|
}
|
||||||
|
|
||||||
BOLD_PALETTE: dict[
|
BOLD_PALETTE = {
|
||||||
str,
|
|
||||||
dict[int, str],
|
|
||||||
] = {
|
|
||||||
'bold': {
|
'bold': {
|
||||||
level: f"bold_{color}" for level, color in STD_PALETTE.items()}
|
level: f"bold_{color}" for level, color in STD_PALETTE.items()}
|
||||||
}
|
}
|
||||||
|
|
@ -116,26 +97,9 @@ def at_least_level(
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
# TODO, compare with using a "filter" instead?
|
# TODO: this isn't showing the correct '{filename}'
|
||||||
# - https://stackoverflow.com/questions/60691759/add-information-to-every-log-message-in-python-logging/61830838#61830838
|
# as it did before..
|
||||||
# |_corresponding dict-config,
|
|
||||||
# https://stackoverflow.com/questions/7507825/where-is-a-complete-example-of-logging-config-dictconfig/7507842#7507842
|
|
||||||
# - [ ] what's the benefit/tradeoffs?
|
|
||||||
#
|
|
||||||
class StackLevelAdapter(LoggerAdapter):
|
class StackLevelAdapter(LoggerAdapter):
|
||||||
'''
|
|
||||||
A (software) stack oriented logger "adapter".
|
|
||||||
|
|
||||||
'''
|
|
||||||
@property
|
|
||||||
def level(self) -> str:
|
|
||||||
'''
|
|
||||||
The currently set `str` emit level (in lowercase).
|
|
||||||
|
|
||||||
'''
|
|
||||||
return logging.getLevelName(
|
|
||||||
self.getEffectiveLevel()
|
|
||||||
).lower()
|
|
||||||
|
|
||||||
def at_least_level(
|
def at_least_level(
|
||||||
self,
|
self,
|
||||||
|
|
@ -284,14 +248,9 @@ def pformat_task_uid(
|
||||||
return f'{task.name}[{tid_part}]'
|
return f'{task.name}[{tid_part}]'
|
||||||
|
|
||||||
|
|
||||||
_curr_actor_no_exc = partial(
|
|
||||||
current_actor,
|
|
||||||
err_on_no_runtime=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
_conc_name_getters = {
|
_conc_name_getters = {
|
||||||
'task': pformat_task_uid,
|
'task': pformat_task_uid,
|
||||||
'actor': lambda: _curr_actor_no_exc(),
|
'actor': lambda: current_actor(),
|
||||||
'actor_name': lambda: current_actor().name,
|
'actor_name': lambda: current_actor().name,
|
||||||
'actor_uid': lambda: current_actor().uid[1][:6],
|
'actor_uid': lambda: current_actor().uid[1][:6],
|
||||||
}
|
}
|
||||||
|
|
@ -323,16 +282,9 @@ class ActorContextInfo(Mapping):
|
||||||
return f'no {key} context'
|
return f'no {key} context'
|
||||||
|
|
||||||
|
|
||||||
_proj_name: str = 'tractor'
|
|
||||||
|
|
||||||
|
|
||||||
def get_logger(
|
def get_logger(
|
||||||
name: str|None = None,
|
name: str|None = None,
|
||||||
# ^NOTE, setting `name=_proj_name=='tractor'` enables the "root
|
_root_name: str = _proj_name,
|
||||||
# logger" for `tractor` itself.
|
|
||||||
pkg_name: str = _proj_name,
|
|
||||||
# XXX, deprecated, use ^
|
|
||||||
_root_name: str|None = None,
|
|
||||||
|
|
||||||
logger: Logger|None = None,
|
logger: Logger|None = None,
|
||||||
|
|
||||||
|
|
@ -341,129 +293,23 @@ def get_logger(
|
||||||
# |_https://stackoverflow.com/questions/7507825/where-is-a-complete-example-of-logging-config-dictconfig
|
# |_https://stackoverflow.com/questions/7507825/where-is-a-complete-example-of-logging-config-dictconfig
|
||||||
# |_https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema
|
# |_https://docs.python.org/3/library/logging.config.html#configuration-dictionary-schema
|
||||||
subsys_spec: str|None = None,
|
subsys_spec: str|None = None,
|
||||||
mk_sublog: bool = True,
|
|
||||||
_strict_debug: bool = False,
|
|
||||||
|
|
||||||
) -> StackLevelAdapter:
|
) -> StackLevelAdapter:
|
||||||
'''
|
'''
|
||||||
Return the `tractor`-library root logger or a sub-logger for
|
Return the `tractor`-library root logger or a sub-logger for
|
||||||
`name` if provided.
|
`name` if provided.
|
||||||
|
|
||||||
When `name` is left null we try to auto-detect the caller's
|
|
||||||
`mod.__name__` and use that as a the sub-logger key.
|
|
||||||
This allows for example creating a module level instance like,
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
log = tractor.log.get_logger(_root_name='mylib')
|
|
||||||
|
|
||||||
and by default all console record headers will show the caller's
|
|
||||||
(of any `log.<level>()`-method) correct sub-pkg's
|
|
||||||
+ py-module-file.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if _root_name:
|
|
||||||
msg: str = (
|
|
||||||
'The `_root_name: str` param of `get_logger()` is now deprecated.\n'
|
|
||||||
'Use the new `pkg_name: str` instead, it is the same usage.\n'
|
|
||||||
)
|
|
||||||
warnings.warn(
|
|
||||||
msg,
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
|
|
||||||
pkg_name: str = _root_name
|
|
||||||
|
|
||||||
def get_caller_mod(
|
|
||||||
frames_up:int = 2
|
|
||||||
):
|
|
||||||
'''
|
|
||||||
Attempt to get the module which called `tractor.get_logger()`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
callstack: list[FrameInfo] = stack()
|
|
||||||
caller_fi: FrameInfo = callstack[frames_up]
|
|
||||||
caller_mod: ModuleType = getmodule(caller_fi.frame)
|
|
||||||
return caller_mod
|
|
||||||
|
|
||||||
# --- Auto--naming-CASE ---
|
|
||||||
# -------------------------
|
|
||||||
# Implicitly introspect the caller's module-name whenever `name`
|
|
||||||
# if left as the null default.
|
|
||||||
#
|
|
||||||
# When the `pkg_name` is `in` in the `mod.__name__` we presume
|
|
||||||
# this instance can be created as a sub-`StackLevelAdapter` and
|
|
||||||
# that the intention is to get free module-path tracing and
|
|
||||||
# filtering (well once we implement that) oriented around the
|
|
||||||
# py-module code hierarchy of the consuming project.
|
|
||||||
#
|
|
||||||
if (
|
|
||||||
mk_sublog
|
|
||||||
and
|
|
||||||
name is None
|
|
||||||
and
|
|
||||||
pkg_name
|
|
||||||
):
|
|
||||||
if (caller_mod := get_caller_mod()):
|
|
||||||
# ?XXX how is this `caller_mod.__name__` defined?
|
|
||||||
# => well by how the mod is imported.. XD
|
|
||||||
# |_https://stackoverflow.com/a/15883682
|
|
||||||
#
|
|
||||||
# if pkg_name in caller_mod.__package__:
|
|
||||||
# from tractor.devx.debug import mk_pdb
|
|
||||||
# mk_pdb().set_trace()
|
|
||||||
|
|
||||||
mod_ns_path: str = caller_mod.__name__
|
|
||||||
mod_pkg_ns_path: str = caller_mod.__package__
|
|
||||||
if (
|
|
||||||
mod_pkg_ns_path in mod_ns_path
|
|
||||||
or
|
|
||||||
pkg_name in mod_ns_path
|
|
||||||
):
|
|
||||||
# proper_mod_name = mod_ns_path.lstrip(
|
|
||||||
proper_mod_name = mod_pkg_ns_path.removeprefix(
|
|
||||||
f'{pkg_name}.'
|
|
||||||
)
|
|
||||||
name = proper_mod_name
|
|
||||||
|
|
||||||
elif (
|
|
||||||
pkg_name
|
|
||||||
# and
|
|
||||||
# pkg_name in mod_ns_path
|
|
||||||
):
|
|
||||||
name = mod_ns_path
|
|
||||||
|
|
||||||
if _strict_debug:
|
|
||||||
msg: str = (
|
|
||||||
f'@ {get_caller_mod()}\n'
|
|
||||||
f'Generating sub-logger name,\n'
|
|
||||||
f'{pkg_name}.{name}\n'
|
|
||||||
)
|
|
||||||
if _curr_actor_no_exc():
|
|
||||||
_root_log.debug(msg)
|
|
||||||
elif pkg_name != _proj_name:
|
|
||||||
print(
|
|
||||||
f'=> tractor.log.get_logger():\n'
|
|
||||||
f'{msg}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
# build a root logger instance
|
|
||||||
log: Logger
|
log: Logger
|
||||||
rlog = log = (
|
log = rlog = logger or logging.getLogger(_root_name)
|
||||||
logger
|
|
||||||
or
|
|
||||||
logging.getLogger(pkg_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
# XXX, lowlevel debuggin..
|
if (
|
||||||
# if pkg_name != _proj_name:
|
name
|
||||||
# from tractor.devx.debug import mk_pdb
|
and
|
||||||
# mk_pdb().set_trace()
|
name != _proj_name
|
||||||
|
):
|
||||||
|
|
||||||
# NOTE: for handling for modules that use the unecessary,
|
# NOTE: for handling for modules that use `get_logger(__name__)`
|
||||||
# `get_logger(__name__)`
|
|
||||||
#
|
|
||||||
# we make the following stylistic choice:
|
# we make the following stylistic choice:
|
||||||
# - always avoid duplicate project-package token
|
# - always avoid duplicate project-package token
|
||||||
# in msg output: i.e. tractor.tractor.ipc._chan.py in header
|
# in msg output: i.e. tractor.tractor.ipc._chan.py in header
|
||||||
|
|
@ -471,157 +317,25 @@ def get_logger(
|
||||||
# - never show the leaf module name in the {name} part
|
# - never show the leaf module name in the {name} part
|
||||||
# since in python the {filename} is always this same
|
# since in python the {filename} is always this same
|
||||||
# module-file.
|
# module-file.
|
||||||
if (
|
|
||||||
name
|
|
||||||
and
|
|
||||||
# ?TODO? more correct?
|
|
||||||
# _proj_name not in name
|
|
||||||
name != pkg_name
|
|
||||||
):
|
|
||||||
# ex. modden.runtime.progman
|
|
||||||
# -> rname='modden', _, pkg_path='runtime.progman'
|
|
||||||
if (
|
|
||||||
pkg_name
|
|
||||||
and
|
|
||||||
pkg_name in name
|
|
||||||
):
|
|
||||||
proper_name: str = name.removeprefix(
|
|
||||||
f'{pkg_name}.'
|
|
||||||
)
|
|
||||||
msg: str = (
|
|
||||||
f'@ {get_caller_mod()}\n'
|
|
||||||
f'Duplicate pkg-name in sub-logger `name`-key?\n'
|
|
||||||
f'pkg_name = {pkg_name!r}\n'
|
|
||||||
f'name = {name!r}\n'
|
|
||||||
f'\n'
|
|
||||||
f'=> You should change your input params to,\n'
|
|
||||||
f'get_logger(\n'
|
|
||||||
f' pkg_name={pkg_name!r}\n'
|
|
||||||
f' name={proper_name!r}\n'
|
|
||||||
f')'
|
|
||||||
)
|
|
||||||
# assert _duplicate == rname
|
|
||||||
if _curr_actor_no_exc():
|
|
||||||
_root_log.warning(msg)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f'=> tractor.log.get_logger() ERROR:\n'
|
|
||||||
f'{msg}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
name = proper_name
|
sub_name: None|str = None
|
||||||
|
rname, _, sub_name = name.partition('.')
|
||||||
|
pkgpath, _, modfilename = sub_name.rpartition('.')
|
||||||
|
|
||||||
rname: str = pkg_name
|
# NOTE: for tractor itself never include the last level
|
||||||
pkg_path: str = name
|
# module key in the name such that something like: eg.
|
||||||
|
# 'tractor.trionics._broadcast` only includes the first
|
||||||
|
# 2 tokens in the (coloured) name part.
|
||||||
|
if rname == 'tractor':
|
||||||
|
sub_name = pkgpath
|
||||||
|
|
||||||
|
if _root_name in sub_name:
|
||||||
|
duplicate, _, sub_name = sub_name.partition('.')
|
||||||
|
|
||||||
# (
|
if not sub_name:
|
||||||
# rname,
|
|
||||||
# _,
|
|
||||||
# pkg_path,
|
|
||||||
# ) = name.partition('.')
|
|
||||||
|
|
||||||
# For ex. 'modden.runtime.progman'
|
|
||||||
# -> pkgpath='runtime', _, leaf_mod='progman'
|
|
||||||
(
|
|
||||||
subpkg_path,
|
|
||||||
_,
|
|
||||||
leaf_mod,
|
|
||||||
) = pkg_path.rpartition('.')
|
|
||||||
|
|
||||||
# NOTE: special usage for passing `name=__name__`,
|
|
||||||
#
|
|
||||||
# - remove duplication of any root-pkg-name in the
|
|
||||||
# (sub/child-)logger name; i.e. never include the
|
|
||||||
# `pkg_name` *twice* in the top-most-pkg-name/level
|
|
||||||
#
|
|
||||||
# -> this happens normally since it is added to `.getChild()`
|
|
||||||
# and as the name of its root-logger.
|
|
||||||
#
|
|
||||||
# => So for ex. (module key in the name) something like
|
|
||||||
# `name='tractor.trionics._broadcast` is passed,
|
|
||||||
# only includes the first 2 sub-pkg name-tokens in the
|
|
||||||
# child-logger's name; the colored "pkg-namespace" header
|
|
||||||
# will then correctly show the same value as `name`.
|
|
||||||
if (
|
|
||||||
# XXX, TRY to remove duplication cases
|
|
||||||
# which get warn-logged on below!
|
|
||||||
(
|
|
||||||
# when, subpkg_path == pkg_path
|
|
||||||
subpkg_path
|
|
||||||
and
|
|
||||||
rname == pkg_name
|
|
||||||
)
|
|
||||||
# ) or (
|
|
||||||
# # when, pkg_path == leaf_mod
|
|
||||||
# pkg_path
|
|
||||||
# and
|
|
||||||
# leaf_mod == pkg_path
|
|
||||||
# )
|
|
||||||
):
|
|
||||||
pkg_path = subpkg_path
|
|
||||||
|
|
||||||
# XXX, do some double-checks for duplication of,
|
|
||||||
# - root-pkg-name, already in root logger
|
|
||||||
# - leaf-module-name already in `{filename}` header-field
|
|
||||||
if (
|
|
||||||
_strict_debug
|
|
||||||
and
|
|
||||||
pkg_name
|
|
||||||
and
|
|
||||||
pkg_name in pkg_path
|
|
||||||
):
|
|
||||||
_duplicate, _, pkg_path = pkg_path.partition('.')
|
|
||||||
if _duplicate:
|
|
||||||
msg: str = (
|
|
||||||
f'@ {get_caller_mod()}\n'
|
|
||||||
f'Duplicate pkg-name in sub-logger key?\n'
|
|
||||||
f'pkg_name = {pkg_name!r}\n'
|
|
||||||
f'pkg_path = {pkg_path!r}\n'
|
|
||||||
)
|
|
||||||
# assert _duplicate == rname
|
|
||||||
if _curr_actor_no_exc():
|
|
||||||
_root_log.warning(msg)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f'=> tractor.log.get_logger() ERROR:\n'
|
|
||||||
f'{msg}\n'
|
|
||||||
)
|
|
||||||
# XXX, should never get here?
|
|
||||||
breakpoint()
|
|
||||||
if (
|
|
||||||
_strict_debug
|
|
||||||
and
|
|
||||||
leaf_mod
|
|
||||||
and
|
|
||||||
leaf_mod in pkg_path
|
|
||||||
):
|
|
||||||
msg: str = (
|
|
||||||
f'@ {get_caller_mod()}\n'
|
|
||||||
f'Duplicate leaf-module-name in sub-logger key?\n'
|
|
||||||
f'leaf_mod = {leaf_mod!r}\n'
|
|
||||||
f'pkg_path = {pkg_path!r}\n'
|
|
||||||
)
|
|
||||||
if _curr_actor_no_exc():
|
|
||||||
_root_log.warning(msg)
|
|
||||||
else:
|
|
||||||
print(
|
|
||||||
f'=> tractor.log.get_logger() ERROR:\n'
|
|
||||||
f'{msg}\n'
|
|
||||||
)
|
|
||||||
|
|
||||||
# mk/get underlying (sub-)`Logger`
|
|
||||||
if (
|
|
||||||
not pkg_path
|
|
||||||
and
|
|
||||||
leaf_mod == pkg_name
|
|
||||||
):
|
|
||||||
# breakpoint()
|
|
||||||
log = rlog
|
log = rlog
|
||||||
|
else:
|
||||||
elif mk_sublog:
|
log = rlog.getChild(sub_name)
|
||||||
# breakpoint()
|
|
||||||
log = rlog.getChild(pkg_path)
|
|
||||||
|
|
||||||
log.level = rlog.level
|
log.level = rlog.level
|
||||||
|
|
||||||
|
|
@ -636,13 +350,8 @@ def get_logger(
|
||||||
for name, val in CUSTOM_LEVELS.items():
|
for name, val in CUSTOM_LEVELS.items():
|
||||||
logging.addLevelName(val, name)
|
logging.addLevelName(val, name)
|
||||||
|
|
||||||
# ensure our custom adapter levels exist as methods
|
# ensure customs levels exist as methods
|
||||||
assert getattr(
|
assert getattr(logger, name.lower()), f'Logger does not define {name}'
|
||||||
logger,
|
|
||||||
name.lower()
|
|
||||||
), (
|
|
||||||
f'Logger does not define {name}'
|
|
||||||
)
|
|
||||||
|
|
||||||
return logger
|
return logger
|
||||||
|
|
||||||
|
|
@ -716,4 +425,4 @@ def get_loglevel() -> str:
|
||||||
|
|
||||||
|
|
||||||
# global module logger for tractor itself
|
# global module logger for tractor itself
|
||||||
_root_log: StackLevelAdapter = get_logger('tractor')
|
log: StackLevelAdapter = get_logger('tractor')
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ from tractor.log import get_logger
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from tractor._context import Context
|
from tractor._context import Context
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# TODO: unify with `MsgCodec` by making `._dec` part this?
|
# TODO: unify with `MsgCodec` by making `._dec` part this?
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ if TYPE_CHECKING:
|
||||||
from tractor._streaming import MsgStream
|
from tractor._streaming import MsgStream
|
||||||
|
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
_def_any_pldec: MsgDec[Any] = mk_dec(spec=Any)
|
_def_any_pldec: MsgDec[Any] = mk_dec(spec=Any)
|
||||||
|
|
|
||||||
|
|
@ -126,17 +126,13 @@ def iter_struct_ppfmt_lines(
|
||||||
str(ft)
|
str(ft)
|
||||||
).replace(' ', '')
|
).replace(' ', '')
|
||||||
|
|
||||||
if k[0] == '_':
|
|
||||||
continue
|
|
||||||
|
|
||||||
# recurse to get sub-struct's `.pformat()` output Bo
|
# recurse to get sub-struct's `.pformat()` output Bo
|
||||||
elif isinstance(v, Struct):
|
if isinstance(v, Struct):
|
||||||
yield from iter_struct_ppfmt_lines(
|
yield from iter_struct_ppfmt_lines(
|
||||||
struct=v,
|
struct=v,
|
||||||
field_indent=field_indent+field_indent,
|
field_indent=field_indent+field_indent,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
else: # top-level field
|
|
||||||
val_str: str = repr(v)
|
val_str: str = repr(v)
|
||||||
|
|
||||||
# XXX LOL, below just seems to be f#$%in causing
|
# XXX LOL, below just seems to be f#$%in causing
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ from tractor.log import get_logger
|
||||||
# from tractor._addr import UnwrappedAddress
|
# from tractor._addr import UnwrappedAddress
|
||||||
|
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger('tractor.msgspec')
|
||||||
|
|
||||||
# type variable for the boxed payload field `.pld`
|
# type variable for the boxed payload field `.pld`
|
||||||
PayloadT = TypeVar('PayloadT')
|
PayloadT = TypeVar('PayloadT')
|
||||||
|
|
@ -202,10 +202,7 @@ class SpawnSpec(
|
||||||
# TODO: similar to the `Start` kwargs spec needed below, we need
|
# TODO: similar to the `Start` kwargs spec needed below, we need
|
||||||
# a hard `Struct` def for all of these fields!
|
# a hard `Struct` def for all of these fields!
|
||||||
_parent_main_data: dict
|
_parent_main_data: dict
|
||||||
_runtime_vars: (
|
_runtime_vars: dict[str, Any]
|
||||||
dict[str, Any]
|
|
||||||
#|RuntimeVars # !TODO
|
|
||||||
)
|
|
||||||
# ^NOTE see `._state._runtime_vars: dict`
|
# ^NOTE see `._state._runtime_vars: dict`
|
||||||
|
|
||||||
# module import capability
|
# module import capability
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ from tractor._state import (
|
||||||
_runtime_vars,
|
_runtime_vars,
|
||||||
)
|
)
|
||||||
from tractor._context import Unresolved
|
from tractor._context import Unresolved
|
||||||
from tractor import devx
|
from tractor.devx import debug
|
||||||
from tractor.log import (
|
from tractor.log import (
|
||||||
get_logger,
|
get_logger,
|
||||||
StackLevelAdapter,
|
StackLevelAdapter,
|
||||||
|
|
@ -71,7 +71,7 @@ from outcome import (
|
||||||
Outcome,
|
Outcome,
|
||||||
)
|
)
|
||||||
|
|
||||||
log: StackLevelAdapter = get_logger()
|
log: StackLevelAdapter = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|
@ -94,14 +94,10 @@ else:
|
||||||
QueueShutDown = False
|
QueueShutDown = False
|
||||||
|
|
||||||
|
|
||||||
# TODO, generally speaking we can generalize this abstraction as,
|
# TODO, generally speaking we can generalize this abstraction, a "SC linked
|
||||||
#
|
# parent->child task pair", as the same "supervision scope primitive"
|
||||||
# > A "SC linked, inter-event-loop" channel for comms between
|
# **that is** our `._context.Context` with the only difference being
|
||||||
# > a `parent: trio.Task` -> `child: asyncio.Task` pair.
|
# in how the tasks conduct msg-passing comms.
|
||||||
#
|
|
||||||
# It is **very similar** in terms of its operation as a "supervision
|
|
||||||
# scope primitive" to that of our `._context.Context` with the only
|
|
||||||
# difference being in how the tasks conduct msg-passing comms.
|
|
||||||
#
|
#
|
||||||
# For `LinkedTaskChannel` we are passing the equivalent of (once you
|
# For `LinkedTaskChannel` we are passing the equivalent of (once you
|
||||||
# include all the recently added `._trio/aio_to_raise`
|
# include all the recently added `._trio/aio_to_raise`
|
||||||
|
|
@ -126,7 +122,6 @@ class LinkedTaskChannel(
|
||||||
task scheduled in the host loop.
|
task scheduled in the host loop.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# ?TODO, rename as `._aio_q` since it's 2-way?
|
|
||||||
_to_aio: asyncio.Queue
|
_to_aio: asyncio.Queue
|
||||||
_from_aio: trio.MemoryReceiveChannel
|
_from_aio: trio.MemoryReceiveChannel
|
||||||
|
|
||||||
|
|
@ -240,11 +235,9 @@ class LinkedTaskChannel(
|
||||||
#
|
#
|
||||||
async def receive(self) -> Any:
|
async def receive(self) -> Any:
|
||||||
'''
|
'''
|
||||||
Receive a value `trio.Task` <- `asyncio.Task`.
|
Receive a value from the paired `asyncio.Task` with
|
||||||
|
|
||||||
Note the tasks in each loop are "SC linked" as a pair with
|
|
||||||
exception/cancel handling to teardown both sides on any
|
exception/cancel handling to teardown both sides on any
|
||||||
unexpected error or cancellation.
|
unexpected error.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
try:
|
try:
|
||||||
|
|
@ -268,40 +261,15 @@ class LinkedTaskChannel(
|
||||||
):
|
):
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
async def get(self) -> Any:
|
|
||||||
'''
|
|
||||||
Receive a value `asyncio.Task` <- `trio.Task`.
|
|
||||||
|
|
||||||
This is equiv to `await self._to_aio.get()`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
return await self._to_aio.get()
|
|
||||||
|
|
||||||
async def send(self, item: Any) -> None:
|
async def send(self, item: Any) -> None:
|
||||||
'''
|
'''
|
||||||
Send a value `trio.Task` -> `asyncio.Task`
|
Send a value through to the asyncio task presuming
|
||||||
by enqueuing `item` onto the internal
|
it defines a ``from_trio`` argument, if it does not
|
||||||
`asyncio.Queue` via `put_nowait()`.
|
this method will raise an error.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
self._to_aio.put_nowait(item)
|
self._to_aio.put_nowait(item)
|
||||||
|
|
||||||
# TODO? could we only compile-in this method on an instance
|
|
||||||
# handed to the `asyncio`-side, i.e. the fn invoked with
|
|
||||||
# `.open_channel_from()`.
|
|
||||||
def send_nowait(
|
|
||||||
self,
|
|
||||||
item: Any,
|
|
||||||
) -> None:
|
|
||||||
'''
|
|
||||||
Send a value through FROM the `asyncio.Task` to
|
|
||||||
the `trio.Task` NON-BLOCKING.
|
|
||||||
|
|
||||||
This is equiv to `self._to_trio.send_nowait()`.
|
|
||||||
|
|
||||||
'''
|
|
||||||
self._to_trio.send_nowait(item)
|
|
||||||
|
|
||||||
# TODO? needed?
|
# TODO? needed?
|
||||||
# async def wait_aio_complete(self) -> None:
|
# async def wait_aio_complete(self) -> None:
|
||||||
# await self._aio_task_complete.wait()
|
# await self._aio_task_complete.wait()
|
||||||
|
|
@ -369,12 +337,9 @@ def _run_asyncio_task(
|
||||||
|
|
||||||
'''
|
'''
|
||||||
__tracebackhide__: bool = hide_tb
|
__tracebackhide__: bool = hide_tb
|
||||||
if not (actor := tractor.current_actor()).is_infected_aio():
|
if not tractor.current_actor().is_infected_aio():
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f'`infect_asyncio: bool` mode is not enabled ??\n'
|
"`infect_asyncio` mode is not enabled!?"
|
||||||
f'Ensure you pass `ActorNursery.start_actor(infect_asyncio=True)`\n'
|
|
||||||
f'\n'
|
|
||||||
f'{actor}\n'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# ITC (inter task comms), these channel/queue names are mostly from
|
# ITC (inter task comms), these channel/queue names are mostly from
|
||||||
|
|
@ -437,23 +402,7 @@ def _run_asyncio_task(
|
||||||
|
|
||||||
orig = result = id(coro)
|
orig = result = id(coro)
|
||||||
try:
|
try:
|
||||||
# XXX TODO UGH!
|
|
||||||
# this seems to break a `test_sync_pause_from_aio_task`
|
|
||||||
# in a REALLY weird way where a `dict` value for
|
|
||||||
# `_runtime_vars['_root_addrs']` is delivered from the
|
|
||||||
# parent actor??
|
|
||||||
#
|
|
||||||
# XXX => see masked `.set_trace()` block in
|
|
||||||
# `Actor.from_parent()`..
|
|
||||||
#
|
|
||||||
# with devx.maybe_open_crash_handler(
|
|
||||||
# # XXX, if trio-side exits (intentionally) we
|
|
||||||
# # shouldn't care bc it should have its own crash
|
|
||||||
# # handling logic.
|
|
||||||
# ignore={TrioTaskExited,},
|
|
||||||
# ) as _bxerr:
|
|
||||||
result: Any = await coro
|
result: Any = await coro
|
||||||
|
|
||||||
chan._aio_result = result
|
chan._aio_result = result
|
||||||
except BaseException as aio_err:
|
except BaseException as aio_err:
|
||||||
chan._aio_err = aio_err
|
chan._aio_err = aio_err
|
||||||
|
|
@ -560,7 +509,7 @@ def _run_asyncio_task(
|
||||||
if (
|
if (
|
||||||
debug_mode()
|
debug_mode()
|
||||||
and
|
and
|
||||||
(greenback := devx.debug.maybe_import_greenback(
|
(greenback := debug.maybe_import_greenback(
|
||||||
force_reload=True,
|
force_reload=True,
|
||||||
raise_not_found=False,
|
raise_not_found=False,
|
||||||
))
|
))
|
||||||
|
|
@ -960,11 +909,7 @@ async def translate_aio_errors(
|
||||||
except BaseException as _trio_err:
|
except BaseException as _trio_err:
|
||||||
trio_err = chan._trio_err = _trio_err
|
trio_err = chan._trio_err = _trio_err
|
||||||
# await tractor.pause(shield=True) # workx!
|
# await tractor.pause(shield=True) # workx!
|
||||||
|
entered: bool = await debug._maybe_enter_pm(
|
||||||
# !TODO! we need an inter-loop lock here to avoid aio-tasks
|
|
||||||
# clobbering trio ones when both crash in debug-mode!
|
|
||||||
#
|
|
||||||
entered: bool = await devx.debug._maybe_enter_pm(
|
|
||||||
trio_err,
|
trio_err,
|
||||||
api_frame=inspect.currentframe(),
|
api_frame=inspect.currentframe(),
|
||||||
)
|
)
|
||||||
|
|
@ -1298,18 +1243,10 @@ async def open_channel_from(
|
||||||
suppress_graceful_exits: bool = True,
|
suppress_graceful_exits: bool = True,
|
||||||
**target_kwargs,
|
**target_kwargs,
|
||||||
|
|
||||||
) -> AsyncIterator[
|
) -> AsyncIterator[Any]:
|
||||||
tuple[Any, LinkedTaskChannel]
|
|
||||||
]:
|
|
||||||
'''
|
'''
|
||||||
Start an `asyncio.Task` as `target()` and open an
|
Open an inter-loop linked task channel for streaming between a target
|
||||||
inter-loop (linked) channel for streaming between
|
spawned ``asyncio`` task and ``trio``.
|
||||||
it and the current `trio.Task`.
|
|
||||||
|
|
||||||
A pair `(Any, chan: LinkedTaskChannel)` is delivered
|
|
||||||
to the caller where the 1st element is the value
|
|
||||||
provided by the `asyncio.Task`'s unblocking call
|
|
||||||
to `chan.started_nowait()`.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
chan: LinkedTaskChannel = _run_asyncio_task(
|
chan: LinkedTaskChannel = _run_asyncio_task(
|
||||||
|
|
@ -1334,7 +1271,6 @@ async def open_channel_from(
|
||||||
|
|
||||||
# deliver stream handle upward
|
# deliver stream handle upward
|
||||||
yield first, chan
|
yield first, chan
|
||||||
# ^TODO! swap these!!
|
|
||||||
except trio.Cancelled as taskc:
|
except trio.Cancelled as taskc:
|
||||||
if cs.cancel_called:
|
if cs.cancel_called:
|
||||||
if isinstance(chan._trio_to_raise, AsyncioCancelled):
|
if isinstance(chan._trio_to_raise, AsyncioCancelled):
|
||||||
|
|
@ -1365,8 +1301,7 @@ async def open_channel_from(
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# XXX SHOULD NEVER HAPPEN!
|
# XXX SHOULD NEVER HAPPEN!
|
||||||
log.error("SHOULD NEVER GET HERE !?!?")
|
await tractor.pause()
|
||||||
await tractor.pause(shield=True)
|
|
||||||
else:
|
else:
|
||||||
chan._to_trio.close()
|
chan._to_trio.close()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ from trio.lowlevel import current_task
|
||||||
from msgspec import Struct
|
from msgspec import Struct
|
||||||
from tractor.log import get_logger
|
from tractor.log import get_logger
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
# TODO: use new type-vars syntax from 3.12
|
# TODO: use new type-vars syntax from 3.12
|
||||||
# https://realpython.com/python312-new-features/#dedicated-type-variable-syntax
|
# https://realpython.com/python312-new-features/#dedicated-type-variable-syntax
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ if TYPE_CHECKING:
|
||||||
from tractor import ActorNursery
|
from tractor import ActorNursery
|
||||||
|
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
# A regular invariant generic type
|
# A regular invariant generic type
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ from typing import (
|
||||||
import trio
|
import trio
|
||||||
from tractor.log import get_logger
|
from tractor.log import get_logger
|
||||||
|
|
||||||
log = get_logger()
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -246,12 +246,23 @@ async def maybe_raise_from_masking_exc(
|
||||||
type(exc_match) # masked type
|
type(exc_match) # masked type
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add to masked `exc_ctx`
|
|
||||||
if do_warn:
|
if do_warn:
|
||||||
exc_ctx.add_note(note)
|
exc_ctx.add_note(note)
|
||||||
|
|
||||||
# don't unmask already known "special" cases..
|
if (
|
||||||
|
do_warn
|
||||||
|
and
|
||||||
|
type(exc_match) in always_warn_on
|
||||||
|
):
|
||||||
|
log.warning(note)
|
||||||
|
|
||||||
|
if (
|
||||||
|
do_warn
|
||||||
|
and
|
||||||
|
raise_unmasked
|
||||||
|
):
|
||||||
if len(masked) < 2:
|
if len(masked) < 2:
|
||||||
|
# don't unmask already known "special" cases..
|
||||||
if (
|
if (
|
||||||
_mask_cases
|
_mask_cases
|
||||||
and
|
and
|
||||||
|
|
@ -272,26 +283,11 @@ async def maybe_raise_from_masking_exc(
|
||||||
)
|
)
|
||||||
raise exc_match
|
raise exc_match
|
||||||
|
|
||||||
# ^?TODO, see above but, possibly unmasking sub-exc
|
raise exc_ctx from exc_match
|
||||||
|
|
||||||
|
# ??TODO, see above but, possibly unmasking sub-exc
|
||||||
# entries if there are > 1
|
# entries if there are > 1
|
||||||
# else:
|
# else:
|
||||||
# await pause(shield=True)
|
# await pause(shield=True)
|
||||||
|
|
||||||
if type(exc_match) in always_warn_on:
|
|
||||||
import traceback
|
|
||||||
trace: list[str] = traceback.format_exception(
|
|
||||||
type(exc_ctx),
|
|
||||||
exc_ctx,
|
|
||||||
exc_ctx.__traceback__
|
|
||||||
)
|
|
||||||
tb_str: str = ''.join(trace)
|
|
||||||
log.warning(tb_str)
|
|
||||||
# XXX, for debug
|
|
||||||
# from tractor import pause
|
|
||||||
# await pause(shield=True)
|
|
||||||
|
|
||||||
if raise_unmasked:
|
|
||||||
raise exc_ctx from exc_match
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
|
||||||
142
uv.lock
142
uv.lock
|
|
@ -1,6 +1,6 @@
|
||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 2
|
||||||
requires-python = ">=3.12, <3.14"
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
|
|
@ -29,6 +29,18 @@ dependencies = [
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259, upload-time = "2024-09-04T20:43:56.123Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200, upload-time = "2024-09-04T20:43:57.891Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235, upload-time = "2024-09-04T20:44:00.18Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721, upload-time = "2024-09-04T20:44:01.585Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242, upload-time = "2024-09-04T20:44:03.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999, upload-time = "2024-09-04T20:44:05.023Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242, upload-time = "2024-09-04T20:44:06.444Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604, upload-time = "2024-09-04T20:44:08.206Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727, upload-time = "2024-09-04T20:44:09.481Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400, upload-time = "2024-09-04T20:44:10.873Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" },
|
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" },
|
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" },
|
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" },
|
||||||
|
|
@ -94,6 +106,15 @@ version = "3.1.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022, upload-time = "2024-09-20T18:21:04.506Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/2f/ff/df5fede753cc10f6a5be0931204ea30c35fa2f2ea7a35b25bdaf4fe40e46/greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467", size = 186022, upload-time = "2024-09-20T18:21:04.506Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/62/1c2665558618553c42922ed47a4e6d6527e2fa3516a8256c2f431c5d0441/greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70", size = 272479, upload-time = "2024-09-20T17:07:22.332Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/9d/421e2d5f07285b6e4e3a676b016ca781f63cfe4a0cd8eaecf3fd6f7a71ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159", size = 640404, upload-time = "2024-09-20T17:36:45.588Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/de/6e05f5c59262a584e502dd3d261bbdd2c97ab5416cc9c0b91ea38932a901/greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e", size = 652813, upload-time = "2024-09-20T17:39:19.052Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/93/d5f93c84241acdea15a8fd329362c2c71c79e1a507c3f142a5d67ea435ae/greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1", size = 648517, upload-time = "2024-09-20T17:44:24.101Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/85/72f77fc02d00470c86a5c982b8daafdf65d38aefbbe441cebff3bf7037fc/greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383", size = 647831, upload-time = "2024-09-20T17:08:40.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/4b/1c9695aa24f808e156c8f4813f685d975ca73c000c2a5056c514c64980f6/greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a", size = 602413, upload-time = "2024-09-20T17:08:31.728Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/70/ad6e5b31ef330f03b12559d19fda2606a522d3849cde46b24f223d6d1619/greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511", size = 1129619, upload-time = "2024-09-20T17:44:14.222Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/fb/201e1b932e584066e0f0658b538e73c459b34d44b4bd4034f682423bc801/greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395", size = 1155198, upload-time = "2024-09-20T17:09:23.903Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/da/b9ed5e310bb8b89661b80cbcd4db5a067903bbcd7fc854923f5ebb4144f0/greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39", size = 298930, upload-time = "2024-09-20T17:25:18.656Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260, upload-time = "2024-09-20T17:08:07.301Z" },
|
{ url = "https://files.pythonhosted.org/packages/7d/ec/bad1ac26764d26aa1353216fcbfa4670050f66d445448aafa227f8b16e80/greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d", size = 274260, upload-time = "2024-09-20T17:08:07.301Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064, upload-time = "2024-09-20T17:36:47.628Z" },
|
{ url = "https://files.pythonhosted.org/packages/66/d4/c8c04958870f482459ab5956c2942c4ec35cac7fe245527f1039837c17a9/greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79", size = 649064, upload-time = "2024-09-20T17:36:47.628Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420, upload-time = "2024-09-20T17:39:21.258Z" },
|
{ url = "https://files.pythonhosted.org/packages/51/41/467b12a8c7c1303d20abcca145db2be4e6cd50a951fa30af48b6ec607581/greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa", size = 663420, upload-time = "2024-09-20T17:39:21.258Z" },
|
||||||
|
|
@ -145,6 +166,13 @@ version = "0.19.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934, upload-time = "2024-12-27T17:40:28.597Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934, upload-time = "2024-12-27T17:40:28.597Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/d4/2ec2567ac30dab072cce3e91fb17803c52f0a37aab6b0c24375d2b20a581/msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e", size = 187939, upload-time = "2024-12-27T17:39:32.347Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/c0/18226e4328897f4f19875cb62bb9259fe47e901eade9d9376ab5f251a929/msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551", size = 182202, upload-time = "2024-12-27T17:39:33.633Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7", size = 209029, upload-time = "2024-12-27T17:39:35.023Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011", size = 210682, upload-time = "2024-12-27T17:39:36.384Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063", size = 214003, upload-time = "2024-12-27T17:39:39.097Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716", size = 216833, upload-time = "2024-12-27T17:39:41.203Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c", size = 186184, upload-time = "2024-12-27T17:39:43.702Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485, upload-time = "2024-12-27T17:39:44.974Z" },
|
{ url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485, upload-time = "2024-12-27T17:39:44.974Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910, upload-time = "2024-12-27T17:39:46.401Z" },
|
{ url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910, upload-time = "2024-12-27T17:39:46.401Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633, upload-time = "2024-12-27T17:39:49.099Z" },
|
{ url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633, upload-time = "2024-12-27T17:39:49.099Z" },
|
||||||
|
|
@ -184,16 +212,16 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pdbp"
|
name = "pdbp"
|
||||||
version = "1.8.2"
|
version = "1.6.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
{ name = "pygments" },
|
{ name = "pygments" },
|
||||||
{ name = "tabcompleter" },
|
{ name = "tabcompleter" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/50/91/2d614b0db12840d646159f65510415ade0db9db595d6dee3eac60dfe9302/pdbp-1.8.2.tar.gz", hash = "sha256:367c25c17555d3ac1f024b9ad494ff50e6e20f6494a84741487f3e6596d88f94", size = 25843, upload-time = "2026-01-14T03:10:28.134Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/69/13/80da03638f62facbee76312ca9ee5941c017b080f2e4c6919fd4e87e16e3/pdbp-1.6.1.tar.gz", hash = "sha256:f4041642952a05df89664e166d5bd379607a0866ddd753c06874f65552bdf40b", size = 25322, upload-time = "2024-11-07T15:36:43.062Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/fe/53ac0cd932db5dcaf55961bc7cb7afdca8d80d8cc7406ed661f0c7dc111a/pdbp-1.8.2-py3-none-any.whl", hash = "sha256:d4fd05e177636b5ccd0b2e03e378cec57afc06149e5fd975de6f8ddb3d0109a8", size = 21969, upload-time = "2026-01-14T03:10:27.062Z" },
|
{ url = "https://files.pythonhosted.org/packages/29/93/d56fb9ba5569dc29d8263c72e46d21a2fd38741339ebf03f54cf7561828c/pdbp-1.6.1-py3-none-any.whl", hash = "sha256:f10bad2ee044c0e5c168cb0825abfdbdc01c50013e9755df5261b060bdd35c22", size = 21495, upload-time = "2024-11-07T15:36:41.061Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -208,15 +236,6 @@ wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
|
{ url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "platformdirs"
|
|
||||||
version = "4.4.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
|
|
@ -273,11 +292,11 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.19.2"
|
version = "2.19.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" }
|
||||||
wheels = [
|
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" },
|
{ url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -310,32 +329,6 @@ 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" },
|
{ 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 = "ruff"
|
|
||||||
version = "0.14.14"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/2e/06/f71e3a86b2df0dfa2d2f72195941cd09b44f87711cb7fa5193732cb9a5fc/ruff-0.14.14.tar.gz", hash = "sha256:2d0f819c9a90205f3a867dbbd0be083bee9912e170fd7d9704cc8ae45824896b", size = 4515732, upload-time = "2026-01-22T22:30:17.527Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/89/20a12e97bc6b9f9f68343952da08a8099c57237aef953a56b82711d55edd/ruff-0.14.14-py3-none-linux_armv6l.whl", hash = "sha256:7cfe36b56e8489dee8fbc777c61959f60ec0f1f11817e8f2415f429552846aed", size = 10467650, upload-time = "2026-01-22T22:30:08.578Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/b1/c5de3fd2d5a831fcae21beda5e3589c0ba67eec8202e992388e4b17a6040/ruff-0.14.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6006a0082336e7920b9573ef8a7f52eec837add1265cc74e04ea8a4368cd704c", size = 10883245, upload-time = "2026-01-22T22:30:04.155Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/7c/3c1db59a10e7490f8f6f8559d1db8636cbb13dccebf18686f4e3c9d7c772/ruff-0.14.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:026c1d25996818f0bf498636686199d9bd0d9d6341c9c2c3b62e2a0198b758de", size = 10231273, upload-time = "2026-01-22T22:30:34.642Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/6e/5e0e0d9674be0f8581d1f5e0f0a04761203affce3232c1a1189d0e3b4dad/ruff-0.14.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f666445819d31210b71e0a6d1c01e24447a20b85458eea25a25fe8142210ae0e", size = 10585753, upload-time = "2026-01-22T22:30:31.781Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/09/754ab09f46ff1884d422dc26d59ba18b4e5d355be147721bb2518aa2a014/ruff-0.14.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c0f18b922c6d2ff9a5e6c3ee16259adc513ca775bcf82c67ebab7cbd9da5bc8", size = 10286052, upload-time = "2026-01-22T22:30:24.827Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/cc/e71f88dd2a12afb5f50733851729d6b571a7c3a35bfdb16c3035132675a0/ruff-0.14.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1629e67489c2dea43e8658c3dba659edbfd87361624b4040d1df04c9740ae906", size = 11043637, upload-time = "2026-01-22T22:30:13.239Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/b2/397245026352494497dac935d7f00f1468c03a23a0c5db6ad8fc49ca3fb2/ruff-0.14.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:27493a2131ea0f899057d49d303e4292b2cae2bb57253c1ed1f256fbcd1da480", size = 12194761, upload-time = "2026-01-22T22:30:22.542Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5b/06/06ef271459f778323112c51b7587ce85230785cd64e91772034ddb88f200/ruff-0.14.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ff589aab3f5b539e35db38425da31a57521efd1e4ad1ae08fc34dbe30bd7df", size = 12005701, upload-time = "2026-01-22T22:30:20.499Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/d6/99364514541cf811ccc5ac44362f88df66373e9fec1b9d1c4cc830593fe7/ruff-0.14.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc12d74eef0f29f51775f5b755913eb523546b88e2d733e1d701fe65144e89b", size = 11282455, upload-time = "2026-01-22T22:29:59.679Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ca/71/37daa46f89475f8582b7762ecd2722492df26421714a33e72ccc9a84d7a5/ruff-0.14.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb8481604b7a9e75eff53772496201690ce2687067e038b3cc31aaf16aa0b974", size = 11215882, upload-time = "2026-01-22T22:29:57.032Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/10/a31f86169ec91c0705e618443ee74ede0bdd94da0a57b28e72db68b2dbac/ruff-0.14.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:14649acb1cf7b5d2d283ebd2f58d56b75836ed8c6f329664fa91cdea19e76e66", size = 11180549, upload-time = "2026-01-22T22:30:27.175Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/1e/c723f20536b5163adf79bdd10c5f093414293cdf567eed9bdb7b83940f3f/ruff-0.14.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e8058d2145566510790eab4e2fad186002e288dec5e0d343a92fe7b0bc1b3e13", size = 10543416, upload-time = "2026-01-22T22:30:01.964Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/34/8a84cea7e42c2d94ba5bde1d7a4fae164d6318f13f933d92da6d7c2041ff/ruff-0.14.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:e651e977a79e4c758eb807f0481d673a67ffe53cfa92209781dfa3a996cf8412", size = 10285491, upload-time = "2026-01-22T22:30:29.51Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/ef/b7c5ea0be82518906c978e365e56a77f8de7678c8bb6651ccfbdc178c29f/ruff-0.14.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:cc8b22da8d9d6fdd844a68ae937e2a0adf9b16514e9a97cc60355e2d4b219fc3", size = 10733525, upload-time = "2026-01-22T22:30:06.499Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/5b/aaf1dfbcc53a2811f6cc0a1759de24e4b03e02ba8762daabd9b6bd8c59e3/ruff-0.14.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:16bc890fb4cc9781bb05beb5ab4cd51be9e7cb376bf1dd3580512b24eb3fda2b", size = 11315626, upload-time = "2026-01-22T22:30:36.848Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2c/aa/9f89c719c467dfaf8ad799b9bae0df494513fb21d31a6059cb5870e57e74/ruff-0.14.14-py3-none-win32.whl", hash = "sha256:b530c191970b143375b6a68e6f743800b2b786bbcf03a7965b06c4bf04568167", size = 10502442, upload-time = "2026-01-22T22:30:38.93Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/44/90fa543014c45560cae1fffc63ea059fb3575ee6e1cb654562197e5d16fb/ruff-0.14.14-py3-none-win_amd64.whl", hash = "sha256:3dde1435e6b6fe5b66506c1dff67a421d0b7f6488d466f651c07f4cab3bf20fd", size = 11630486, upload-time = "2026-01-22T22:30:10.852Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/6a/40fee331a52339926a92e17ae748827270b288a35ef4a15c9c8f2ec54715/ruff-0.14.14-py3-none-win_arm64.whl", hash = "sha256:56e6981a98b13a32236a72a8da421d7839221fa308b223b9283312312e5ac76c", size = 10920448, upload-time = "2026-01-22T22:30:15.417Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sniffio"
|
name = "sniffio"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
|
|
@ -385,7 +378,6 @@ dependencies = [
|
||||||
{ name = "colorlog" },
|
{ name = "colorlog" },
|
||||||
{ name = "msgspec" },
|
{ name = "msgspec" },
|
||||||
{ name = "pdbp" },
|
{ name = "pdbp" },
|
||||||
{ name = "platformdirs" },
|
|
||||||
{ name = "tricycle" },
|
{ name = "tricycle" },
|
||||||
{ name = "trio" },
|
{ name = "trio" },
|
||||||
{ name = "wrapt" },
|
{ name = "wrapt" },
|
||||||
|
|
@ -403,24 +395,6 @@ dev = [
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
{ name = "xonsh" },
|
{ name = "xonsh" },
|
||||||
]
|
]
|
||||||
devx = [
|
|
||||||
{ name = "greenback" },
|
|
||||||
{ name = "stackscope" },
|
|
||||||
{ name = "typing-extensions" },
|
|
||||||
]
|
|
||||||
lint = [
|
|
||||||
{ name = "ruff" },
|
|
||||||
]
|
|
||||||
repl = [
|
|
||||||
{ name = "prompt-toolkit" },
|
|
||||||
{ name = "psutil" },
|
|
||||||
{ name = "pyperclip" },
|
|
||||||
{ name = "xonsh" },
|
|
||||||
]
|
|
||||||
testing = [
|
|
||||||
{ name = "pexpect" },
|
|
||||||
{ name = "pytest" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
|
@ -428,8 +402,7 @@ requires-dist = [
|
||||||
{ name = "cffi", specifier = ">=1.17.1" },
|
{ name = "cffi", specifier = ">=1.17.1" },
|
||||||
{ name = "colorlog", specifier = ">=6.8.2,<7" },
|
{ name = "colorlog", specifier = ">=6.8.2,<7" },
|
||||||
{ name = "msgspec", specifier = ">=0.19.0" },
|
{ name = "msgspec", specifier = ">=0.19.0" },
|
||||||
{ name = "pdbp", specifier = ">=1.8.2,<2" },
|
{ name = "pdbp", specifier = ">=1.6,<2" },
|
||||||
{ name = "platformdirs", specifier = ">=4.4.0" },
|
|
||||||
{ name = "tricycle", specifier = ">=0.4.1,<0.5" },
|
{ name = "tricycle", specifier = ">=0.4.1,<0.5" },
|
||||||
{ name = "trio", specifier = ">0.27" },
|
{ name = "trio", specifier = ">0.27" },
|
||||||
{ name = "wrapt", specifier = ">=1.16.0,<2" },
|
{ name = "wrapt", specifier = ">=1.16.0,<2" },
|
||||||
|
|
@ -445,23 +418,7 @@ dev = [
|
||||||
{ name = "pytest", specifier = ">=8.3.5" },
|
{ name = "pytest", specifier = ">=8.3.5" },
|
||||||
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
|
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
|
||||||
{ name = "typing-extensions", specifier = ">=4.14.1" },
|
{ name = "typing-extensions", specifier = ">=4.14.1" },
|
||||||
{ name = "xonsh", specifier = ">=0.22.2" },
|
{ name = "xonsh", specifier = ">=0.19.2" },
|
||||||
]
|
|
||||||
devx = [
|
|
||||||
{ name = "greenback", specifier = ">=1.2.1,<2" },
|
|
||||||
{ name = "stackscope", specifier = ">=0.2.2,<0.3" },
|
|
||||||
{ name = "typing-extensions", specifier = ">=4.14.1" },
|
|
||||||
]
|
|
||||||
lint = [{ name = "ruff", specifier = ">=0.9.6" }]
|
|
||||||
repl = [
|
|
||||||
{ name = "prompt-toolkit", specifier = ">=3.0.50" },
|
|
||||||
{ name = "psutil", specifier = ">=7.0.0" },
|
|
||||||
{ name = "pyperclip", specifier = ">=1.9.0" },
|
|
||||||
{ name = "xonsh", specifier = ">=0.22.2" },
|
|
||||||
]
|
|
||||||
testing = [
|
|
||||||
{ name = "pexpect", specifier = ">=4.9.0,<5" },
|
|
||||||
{ name = "pytest", specifier = ">=8.3.5" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -517,6 +474,17 @@ version = "1.17.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531, upload-time = "2025-01-14T10:35:45.465Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/f7/a2aab2cbc7a665efab072344a8949a71081eed1d2f451f7f7d2b966594a2/wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58", size = 53308, upload-time = "2025-01-14T10:33:33.992Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/ff/149aba8365fdacef52b31a258c4dc1c57c79759c335eff0b3316a2664a64/wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda", size = 38488, upload-time = "2025-01-14T10:33:35.264Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/46/5a917ce85b5c3b490d35c02bf71aedaa9f2f63f2d15d9949cc4ba56e8ba9/wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438", size = 38776, upload-time = "2025-01-14T10:33:38.28Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/74/336c918d2915a4943501c77566db41d1bd6e9f4dbc317f356b9a244dfe83/wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a", size = 83776, upload-time = "2025-01-14T10:33:40.678Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/99/c0c844a5ccde0fe5761d4305485297f91d67cf2a1a824c5f282e661ec7ff/wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000", size = 75420, upload-time = "2025-01-14T10:33:41.868Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/b0/9fc566b0fe08b282c850063591a756057c3247b2362b9286429ec5bf1721/wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6", size = 83199, upload-time = "2025-01-14T10:33:43.598Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/4b/71996e62d543b0a0bd95dda485219856def3347e3e9380cc0d6cf10cfb2f/wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b", size = 82307, upload-time = "2025-01-14T10:33:48.499Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/35/0282c0d8789c0dc9bcc738911776c762a701f95cfe113fb8f0b40e45c2b9/wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662", size = 75025, upload-time = "2025-01-14T10:33:51.191Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/6d/90c9fd2c3c6fee181feecb620d95105370198b6b98a0770cba090441a828/wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72", size = 81879, upload-time = "2025-01-14T10:33:52.328Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/fa/9fb6e594f2ce03ef03eddbdb5f4f90acb1452221a5351116c7c4708ac865/wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317", size = 36419, upload-time = "2025-01-14T10:33:53.551Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/f8/fb1773491a253cbc123c5d5dc15c86041f746ed30416535f2a8df1f4a392/wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3", size = 38773, upload-time = "2025-01-14T10:33:56.323Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" },
|
{ url = "https://files.pythonhosted.org/packages/a1/bd/ab55f849fd1f9a58ed7ea47f5559ff09741b25f00c191231f9f059c83949/wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925", size = 53799, upload-time = "2025-01-14T10:33:57.4Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" },
|
{ url = "https://files.pythonhosted.org/packages/53/18/75ddc64c3f63988f5a1d7e10fb204ffe5762bc663f8023f18ecaf31a332e/wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392", size = 38821, upload-time = "2025-01-14T10:33:59.334Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" },
|
{ url = "https://files.pythonhosted.org/packages/48/2a/97928387d6ed1c1ebbfd4efc4133a0633546bec8481a2dd5ec961313a1c7/wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40", size = 38919, upload-time = "2025-01-14T10:34:04.093Z" },
|
||||||
|
|
@ -555,11 +523,13 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xonsh"
|
name = "xonsh"
|
||||||
version = "0.22.4"
|
version = "0.19.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/48/df/1fc9ed62b3d7c14612e1713e9eb7bd41d54f6ad1028a8fbb6b7cddebc345/xonsh-0.22.4.tar.gz", hash = "sha256:6be346563fec2db75778ba5d2caee155525e634e99d9cc8cc347626025c0b3fa", size = 826665, upload-time = "2026-02-17T07:53:39.424Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/68/4e/56e95a5e607eb3b0da37396f87cde70588efc8ef819ab16f02d5b8378dc4/xonsh-0.19.2.tar.gz", hash = "sha256:cfdd0680d954a2c3aefd6caddcc7143a3d06aa417ed18365a08219bb71b960b0", size = 799960, upload-time = "2025-02-11T17:10:43.563Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/00/7cbc0c1fb64365a0a317c54ce3a151c9644eea5a509d9cbaae61c9fd1426/xonsh-0.22.4-py311-none-any.whl", hash = "sha256:38b29b29fa85aa756462d9d9bbcaa1d85478c2108da3de6cc590a69a4bcd1a01", size = 654375, upload-time = "2026-02-17T07:53:37.702Z" },
|
{ url = "https://files.pythonhosted.org/packages/6c/13/281094759df87b23b3c02dc4a16603ab08ea54d7f6acfeb69f3341137c7a/xonsh-0.19.2-py310-none-any.whl", hash = "sha256:ec7f163fd3a4943782aa34069d4e72793328c916a5975949dbec8536cbfc089b", size = 642301, upload-time = "2025-02-11T17:10:39.244Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/c2/3dd498dc28d8f89cdd52e39950c5e591499ae423f61694c0bb4d03ed1d82/xonsh-0.22.4-py312-none-any.whl", hash = "sha256:4e538fac9f4c3d866ddbdeca068f0c0515469c997ed58d3bfee963878c6df5a5", size = 654300, upload-time = "2026-02-17T07:53:35.813Z" },
|
{ url = "https://files.pythonhosted.org/packages/29/41/a51e4c3918fe9a293b150cb949b1b8c6d45eb17dfed480dcb76ea43df4e7/xonsh-0.19.2-py311-none-any.whl", hash = "sha256:53c45f7a767901f2f518f9b8dd60fc653e0498e56e89825e1710bb0859985049", size = 642286, upload-time = "2025-02-11T17:10:41.678Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/7d/1f9c7147518e9f03f6ce081b5bfc4f1aceb6ec5caba849024d005e41d3be/xonsh-0.22.4-py313-none-any.whl", hash = "sha256:cc5fabf0ad0c56a2a11bed1e6a43c4ec6416a5b30f24f126b8e768547c3793e2", size = 654818, upload-time = "2026-02-17T07:53:33.477Z" },
|
{ url = "https://files.pythonhosted.org/packages/0a/93/9a77b731f492fac27c577dea2afb5a2bcc2a6a1c79be0c86c95498060270/xonsh-0.19.2-py312-none-any.whl", hash = "sha256:b24c619aa52b59eae4d35c4195dba9b19a2c548fb5c42c6f85f2b8ccb96807b5", size = 642386, upload-time = "2025-02-11T17:10:43.688Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/75/070324769c1ff88d971ce040f4f486339be98e0a365c8dd9991eb654265b/xonsh-0.19.2-py313-none-any.whl", hash = "sha256:c53ef6c19f781fbc399ed1b382b5c2aac2125010679a3b61d643978273c27df0", size = 642873, upload-time = "2025-02-11T17:10:39.297Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/cb/2c7ccec54f5b0e73fdf7650e8336582ff0347d9001c5ef8271dc00c034fe/xonsh-0.19.2-py39-none-any.whl", hash = "sha256:bcc0225dc3847f1ed2f175dac6122fbcc54cea67d9c2dc2753d9615e2a5ff284", size = 634602, upload-time = "2025-02-11T17:10:37.004Z" },
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue