Skip to content

AttributeError: 'NoneType' object has no attribute 'file' — module-level async def with nested def / async for / async with #31

Open

Description

@nanami7777777

Describe the bug

codegraph (1.2.0, current PyPI release) crashes with

AttributeError: 'NoneType' object has no attribute 'file'
  File ".../codegraph/parser.py", line 74, in _nest_function
    newfunc = Function(func_name, ob.file, lineno, ob)

whenever a module-level async def contains any of:

  • a nested def / async def
  • an async for statement or comprehension
  • an async with statement

Minimal repro

Three lines, no third-party code involved:

async def foo():
    return [x async for x in bar()]
$ codegraph repro.py
AttributeError: 'NoneType' object has no attribute 'file'

Control cases that do not crash (only the module-level async def variant does):

source result
async def + async for crash
def + async for ok
class method async def + async for ok
async def + async with crash
async def + nested def crash
def + nested def ok

Impact

The whole tree is parsed in one pass, so a single offending file makes codegraph <dir> fail for the entire directory. On a real-world Python project this made 16 of 326 modules unparseable, i.e. any directory containing one of them could not be analysed at all.

Root cause

create_objects_array() uses tokenize plus an indentation stack to resolve parent/child relations. Two defects in the async branch (codegraph/parser.py:199-218):

  1. The token after async is never validated. It is discarded via _, _, _ = next(g)[0:3] (comment: "next will be def") and the next NAME is used as the function name, so async for / async with produce a bogus function — in async for x in ... the "name" becomes x.
  2. None is pushed on the stack for a module-level async def. The else branch only does tree.append(AsyncFunction(...)) without assigning cur_func, then stack.append((cur_func, thisindent)) pushes None unconditionally. The synchronous def branch assigns cur_func and guards the push with if cur_func:.

The None stays at the bottom of the stack for the entire function body (indent 0 is never popped by anything at a deeper indent), so the next nested def / async token calls _nest_function(None, ...) → ob.file → AttributeError.

Neither defect crashes on its own — only the combination does, which is why the failure is partial rather than total.

Environment

  • codegraph 1.2.0 (also reproduced on 1.1.0)
  • Python 3.13 / 3.14, macOS

Additional context

The in-flight Release/1.3.0 (#26) replaces the tokenizer parser with an AST-based one; I verified that branch parses all 326 modules of the affected project without error, so 1.3.0 should resolve this on the default path. Two follow-ups:

  • users on the released 1.2.x still hit this today — I opened a PR with a minimal patch for that line;
  • the legacy codegraph/parser.py on the release/1.3.0 branch still carries both defects, so it is worth patching there as well if that path stays reachable.

Activity

  1. nanami7777777 commented on Oct 30, 2026

    @nanami7777777
    Author

    Follow-up: while fixing this on a real project I hit two more defects in the same 1.2.0 line, both now covered by PR #30 (#30).

    2. TypeError: '<=' not supported between instances of 'int' and 'NoneType' in usage_graph()

    Scopes popped by a sibling def / class (a one-line body such as def f(self) -> None: ... emits no INDENT/DEDENT) or still open at EOF never reach the DEDENT handler, so their endno stays None, and core.py then evaluates entity[0] <= line <= entity[1] with entity[1] is None. On the project this affected 74 entities — after the fix, 0.

    3. usage_graph() silently loses every import-derived edge on a second call

    get_imports_and_entities_lines() does code_objects[path].pop(-1) on the Import object, so a second call runs with an empty import set. The CLI builds the graph before searching it, hence --file-path / --distance always printed an empty dependency list. Additionally get_dependencies() compared a relative --file-path against absolute graph keys, and never followed an edge past distance 1 because edges carry bare "<module>.py" names.

    before:  {1: set(), 2: set()}
    after:   {1: {llm.py, types.py, events.py, decorator.py, ...},
              2: {engine.py, context.py, records.py, gates.py, ...}}
    

    Both are fixed in PR #30 with regression tests (46 tests pass, flake8 clean).

    Note: the in-flight Release/1.3.0 (#26) AST parser resolves defect 1 (and likely 2) on the default path — I verified that branch parses all 326 modules of the affected project. Defects 1 and 2 are still present in the legacy codegraph/parser.py on that branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions