pyelftools icon indicating copy to clipboard operation
pyelftools copied to clipboard

Add type hints

Open pmhahn opened this issue 9 months ago • 8 comments

This is the mayor PR to add Python type hints #514 – without #609 this will not be complete as elftools.construct.Container is used in many places, which is a container for Anything: retrieving values from it will be typed Any, which basically means untyped: without manually type-hinting every such use case those values do propagate further and even spill into the public API.

Because of missing 6f99ce09ddf25ce80422f388b7690e6323ccb89f running mypy will find the following errors:

elftools/dwarf/structs.py:581: error: "Container" has no attribute "first"  [attr-defined]
elftools/dwarf/structs.py:583: error: "Container" has no attribute "first"  [attr-defined]
elftools/dwarf/structs.py:585: error: "Container" has no attribute "first"  [attr-defined]
elftools/dwarf/structs.py:587: error: "Container" has no attribute "second"  [attr-defined]
elftools/dwarf/structs.py:590: error: "Container" has no attribute "first"  [attr-defined]
elftools/dwarf/callframe.py:99: error: "Container" has no attribute "length"  [attr-defined]
elftools/dwarf/ranges.py:41: error: "Container" has no attribute "start_index"  [attr-defined]
elftools/dwarf/ranges.py:42: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/ranges.py:42: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/ranges.py:42: error: "Container" has no attribute "length"  [attr-defined]
elftools/dwarf/ranges.py:46: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/ranges.py:46: error: "Container" has no attribute "address"  [attr-defined]
elftools/dwarf/ranges.py:47: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/ranges.py:47: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/ranges.py:47: error: "Container" has no attribute "start_offset"  [attr-defined]
elftools/dwarf/ranges.py:47: error: "Container" has no attribute "end_offset"  [attr-defined]
elftools/dwarf/ranges.py:48: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/ranges.py:48: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/ranges.py:48: error: "Container" has no attribute "start_address"  [attr-defined]
elftools/dwarf/ranges.py:48: error: "Container" has no attribute "end_address"  [attr-defined]
elftools/dwarf/ranges.py:49: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/ranges.py:49: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/ranges.py:49: error: "Container" has no attribute "start_address"  [attr-defined]
elftools/dwarf/ranges.py:49: error: "Container" has no attribute "length"  [attr-defined]
elftools/dwarf/ranges.py:50: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/ranges.py:50: error: "Container" has no attribute "index"  [attr-defined]
elftools/dwarf/ranges.py:51: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/ranges.py:51: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/ranges.py:51: error: "Container" has no attribute "start_index"  [attr-defined]
elftools/dwarf/ranges.py:51: error: "Container" has no attribute "end_index"  [attr-defined]
elftools/dwarf/ranges.py:70: error: "Container" has no attribute "version"  [attr-defined]
elftools/dwarf/ranges.py:180: error: "Container" has no attribute "offset_table_offset"  [attr-defined]
elftools/dwarf/ranges.py:180: error: "Container" has no attribute "is64"  [attr-defined]
elftools/dwarf/ranges.py:180: error: "Container" has no attribute "offset_count"  [attr-defined]
elftools/dwarf/ranges.py:181: error: "Container" has no attribute "offset_after_length"  [attr-defined]
elftools/dwarf/ranges.py:181: error: "Container" has no attribute "unit_length"  [attr-defined]
elftools/dwarf/ranges.py:188: error: "Container" has no attribute "entry_type"  [attr-defined]
elftools/dwarf/locationlists.py:53: error: "Container" has no attribute "start_index"  [attr-defined]
elftools/dwarf/locationlists.py:54: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:54: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/locationlists.py:54: error: "Container" has no attribute "length"  [attr-defined]
elftools/dwarf/locationlists.py:54: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:58: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:58: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/locationlists.py:58: error: "Container" has no attribute "address"  [attr-defined]
elftools/dwarf/locationlists.py:59: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:59: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/locationlists.py:59: error: "Container" has no attribute "start_offset"  [attr-defined]
elftools/dwarf/locationlists.py:59: error: "Container" has no attribute "end_offset"  [attr-defined]
elftools/dwarf/locationlists.py:59: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:60: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:60: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/locationlists.py:60: error: "Container" has no attribute "start_address"  [attr-defined]
elftools/dwarf/locationlists.py:60: error: "Container" has no attribute "length"  [attr-defined]
elftools/dwarf/locationlists.py:60: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:61: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:61: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/locationlists.py:61: error: "Container" has no attribute "start_address"  [attr-defined]
elftools/dwarf/locationlists.py:61: error: "Container" has no attribute "end_address"  [attr-defined]
elftools/dwarf/locationlists.py:61: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:62: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:62: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/locationlists.py:62: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:63: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:63: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/locationlists.py:63: error: "Container" has no attribute "index"  [attr-defined]
elftools/dwarf/locationlists.py:64: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:64: error: "Container" has no attribute "entry_length"  [attr-defined]
elftools/dwarf/locationlists.py:64: error: "Container" has no attribute "start_index"  [attr-defined]
elftools/dwarf/locationlists.py:64: error: "Container" has no attribute "end_index"  [attr-defined]
elftools/dwarf/locationlists.py:64: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:81: error: "Container" has no attribute "version"  [attr-defined]
elftools/dwarf/locationlists.py:222: error: "Container" has no attribute "version"  [attr-defined]
elftools/dwarf/locationlists.py:286: error: "Container" has no attribute "entry_offset"  [attr-defined]
elftools/dwarf/locationlists.py:287: error: "Container" has no attribute "entry_end_offset"  [attr-defined]
elftools/dwarf/locationlists.py:288: error: "Container" has no attribute "entry_type"  [attr-defined]
elftools/dwarf/locationlists.py:290: error: "Container" has no attribute "address"  [attr-defined]
elftools/dwarf/locationlists.py:292: error: "Container" has no attribute "start_offset"  [attr-defined]
elftools/dwarf/locationlists.py:292: error: "Container" has no attribute "end_offset"  [attr-defined]
elftools/dwarf/locationlists.py:292: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:294: error: "Container" has no attribute "start_address"  [attr-defined]
elftools/dwarf/locationlists.py:294: error: "Container" has no attribute "length"  [attr-defined]
elftools/dwarf/locationlists.py:294: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:296: error: "Container" has no attribute "start_address"  [attr-defined]
elftools/dwarf/locationlists.py:296: error: "Container" has no attribute "end_address"  [attr-defined]
elftools/dwarf/locationlists.py:296: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/locationlists.py:298: error: "Container" has no attribute "loc_expr"  [attr-defined]
elftools/dwarf/dwarfinfo.py:478: error: "Container" has no attribute "address_size"  [attr-defined]
elftools/elf/structs.py:429: error: "Container" has no attribute "pr_datasz"  [attr-defined]
elftools/elf/structs.py:430: error: "Container" has no attribute "pr_datasz"  [attr-defined]
elftools/elf/structs.py:433: error: "Container" has no attribute "pr_type"  [attr-defined]
elftools/elf/structs.py:435: error: "Container" has no attribute "pr_type"  [attr-defined]
elftools/elf/structs.py:437: error: "Container" has no attribute "pr_type"  [attr-defined]
elftools/elf/structs.py:439: error: "Container" has no attribute "pr_type"  [attr-defined]
elftools/elf/structs.py:441: error: "Container" has no attribute "pr_type"  [attr-defined]
elftools/elf/structs.py:441: error: "Container" has no attribute "pr_datasz"  [attr-defined]
elftools/elf/notes.py:71: error: "Container" has no attribute "pr_datasz"  [attr-defined]
elftools/elf/dynamic.py:67: error: "Container" has no attribute "d_tag"  [attr-defined]
elftools/elf/dynamic.py:68: error: "Container" has no attribute "d_tag"  [attr-defined]
elftools/elf/dynamic.py:69: error: "Container" has no attribute "d_val"  [attr-defined]
elftools/elf/dynamic.py:77: error: "Container" has no attribute "d_tag"  [attr-defined]
elftools/elf/dynamic.py:80: error: "Container" has no attribute "d_tag"  [attr-defined]
elftools/elf/dynamic.py:81: error: "Container" has no attribute "d_tag"  [attr-defined]
elftools/elf/dynamic.py:83: error: "Container" has no attribute "d_ptr"  [attr-defined]
elftools/elf/dynamic.py:84: error: "Container" has no attribute "d_tag"  [attr-defined]
elftools/elf/dynamic.py:203: error: "Container" has no attribute "d_tag"  [attr-defined]
elftools/elf/elffile.py:152: error: "Container" has no attribute "sh_type"  [attr-defined]
elftools/elf/elffile.py:306: error: "Container" has no attribute "sh_offset"  [attr-defined]
elftools/elf/elffile.py:397: error: "Container" has no attribute "sh_offset"  [attr-defined]
elftools/elf/descriptions.py:59: error: "Container" has no attribute "d_val"  [attr-defined]
elftools/elf/descriptions.py:300: error: "Container" has no attribute "pr_type"  [attr-defined]
elftools/elf/descriptions.py:301: error: "Container" has no attribute "pr_data"  [attr-defined]
elftools/elf/descriptions.py:302: error: "Container" has no attribute "pr_datasz"  [attr-defined]

Similar missing db4fb21cd6040f1cca70ddc4a3fd5e42996f3f0b is responsible for

elftools/elf/enums.py:37: error: Dict entry 2 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:64: error: Dict entry 22 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:76: error: Dict entry 7 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:278: error: Dict entry 187 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:324: error: Dict entry 30 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:393: error: Dict entry 5 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:421: error: Dict entry 14 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:454: error: Dict entry 8 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:473: error: Dict entry 14 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:485: error: Dict entry 7 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:489: error: Dict entry 0 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:497: error: Dict entry 3 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:585: error: Dict entry 83 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:748: error: Dict entry 51 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:795: error: Dict entry 43 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:838: error: Dict entry 39 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:848: error: Dict entry 6 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:951: error: Dict entry 98 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:1023: error: Dict entry 4 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:1042: error: Dict entry 5 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:1054: error: Dict entry 7 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:1065: error: Dict entry 6 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:1077: error: Dict entry 7 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]
elftools/elf/enums.py:1085: error: Dict entry 4 has incompatible type "str": "type[Pass]"; expected "str": "int"  [dict-item]

These I do not know how to fix - their type is not static and depends dynamically on the opened ELF file:

elftools/dwarf/ranges.py:50: error: Cannot determine type of "dwarfinfo"  [has-type]
elftools/dwarf/ranges.py:51: error: Cannot determine type of "dwarfinfo"  [has-type]
elftools/dwarf/locationlists.py:63: error: Cannot determine type of "dwarfinfo"  [has-type]
elftools/dwarf/locationlists.py:64: error: Cannot determine type of "dwarfinfo"  [has-type]
elftools/elf/notes.py:25: error: Cannot determine type of "structs"  [has-type]
elftools/elf/notes.py:30: error: Cannot determine type of "structs"  [has-type]
elftools/elf/notes.py:48: error: Cannot determine type of "structs"  [has-type]
elftools/elf/notes.py:56: error: Cannot determine type of "structs"  [has-type]
elftools/elf/notes.py:60: error: Cannot determine type of "structs"  [has-type]
elftools/elf/notes.py:70: error: Cannot determine type of "structs"  [has-type]
elftools/elf/sections.py:42: error: Cannot determine type of "structs"  [has-type]
elftools/elf/sections.py:180: error: Cannot determine type of "structs"  [has-type]
elftools/elf/relocation.py:63: error: Cannot determine type of "structs"  [has-type]
elftools/elf/relocation.py:131: error: Cannot determine type of "structs"  [has-type]
elftools/elf/relocation.py:185: error: Cannot determine type of "structs"  [has-type]
elftools/elf/relocation.py:345: error: Cannot determine type of "structs"  [has-type]
elftools/elf/relocation.py:347: error: Cannot determine type of "structs"  [has-type]
elftools/elf/relocation.py:349: error: Cannot determine type of "structs"  [has-type]
elftools/elf/relocation.py:351: error: Cannot determine type of "structs"  [has-type]
elftools/elf/hash.py:49: error: Cannot determine type of "structs"  [has-type]
elftools/elf/hash.py:115: error: Cannot determine type of "structs"  [has-type]
elftools/elf/hash.py:120: error: Cannot determine type of "structs"  [has-type]
elftools/elf/hash.py:121: error: Cannot determine type of "structs"  [has-type]
elftools/elf/gnuversions.py:153: error: Cannot determine type of "structs"  [has-type]
elftools/elf/gnuversions.py:196: error: Cannot determine type of "structs"  [has-type]
elftools/elf/dynamic.py:110: error: Cannot determine type of "structs"  [has-type]

And finally the last group of issues, which are also caused by missing cc7b1eaa277fd:

elftools/dwarf/structs.py:413: error: Unused "type: ignore" comment  [unused-ignore]
elftools/common/utils.py:41: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/common/utils.py:41: error: A function returning TypeVar should receive at least one argument containing the same TypeVar  [type-var]
elftools/elf/structs.py:54: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/elf/structs.py:55: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/elf/structs.py:56: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/elf/structs.py:57: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/elf/structs.py:58: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/elf/structs.py:59: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/elf/structs.py:60: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/elf/structs.py:61: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/elf/structs.py:62: error: "FormatField" expects no type arguments, but 1 given  [type-arg]
elftools/dwarf/descriptions.py:93: error: Incompatible types in string interpolation (expression has type "int | None", placeholder has type "int")  [str-format]
elftools/dwarf/descriptions.py:135: error: Incompatible types in string interpolation (expression has type "ListContainer", placeholder has type "int | float | SupportsInt")  [str-format]
elftools/dwarf/descriptions.py:135: error: Incompatible types in string interpolation (expression has type "None", placeholder has type "int | float | SupportsInt")  [str-format]
elftools/dwarf/descriptions.py:149: error: Incompatible types in string interpolation (expression has type "int | None", placeholder has type "int | float | SupportsInt")  [str-format]
elftools/ehabi/ehabiinfo.py:58: error: Incompatible types in string interpolation (expression has type "int | None", placeholder has type "int | float | SupportsInt")  [str-format]
elftools/ehabi/ehabiinfo.py:164: error: Incompatible types in string interpolation (expression has type "int | None", placeholder has type "int")  [str-format]
elftools/ehabi/ehabiinfo.py:164: error: Incompatible types in string interpolation (expression has type "int | None", placeholder has type "int | float | SupportsInt")  [str-format]
elftools/ehabi/ehabiinfo.py:193: error: Incompatible types in string interpolation (expression has type "None", placeholder has type "int")  [str-format]
elftools/ehabi/ehabiinfo.py:204: error: Incompatible types in string interpolation (expression has type "int | None", placeholder has type "int")  [str-format]
elftools/elf/sections.py:106: error: Incompatible types in string interpolation (expression has type "str", placeholder has type "int")  [str-format]
elftools/elf/descriptions.py:349: error: Incompatible types in string interpolation (expression has type "str | int", placeholder has type "int")  [str-format]

Please have a 1st look.

Then we can decide on how to proceed, e.g. just merge it or try to extract a subset for only some public API files.

pmhahn avatar Mar 19 '25 06:03 pmhahn

One question and general comment: would it be possible to split this PR to multiple? Even if just for the sake of review - could we start with a small-medium PR with a representative set of changes? It's OK if the intermediate steps don't fully type check until everything has landed

Yes, I can do that. Any advise on how to split best? Internel / low-level / high-level?

pmhahn avatar Apr 01 '25 05:04 pmhahn

One question and general comment: would it be possible to split this PR to multiple? Even if just for the sake of review - could we start with a small-medium PR with a representative set of changes? It's OK if the intermediate steps don't fully type check until everything has landed

Yes, I can do that. Any advise on how to split best? Internel / low-level / high-level?

Yes, by layers could be a great way to slice it. Starting at the lowest possible and then progressing upwards

eliben avatar Apr 01 '25 12:04 eliben

Where do we stand with this PR? How much of it has already been added?

eliben avatar May 05 '25 17:05 eliben

Where do we stand with this PR? How much of it has already been added?

Sorry for the delay, I'm currently busy otherwise. Hope to find some time next weekend.

pmhahn avatar May 08 '25 11:05 pmhahn

What's the status here?

sevaa avatar Aug 28 '25 14:08 sevaa

What's the status here?

The bad news: Sadly I'm busy otherwise . The good news: I found some time to update the PR and it again is in a state, where it could be merged:

pyright got unhappy, so I again had to re-introduce 6cb25b9 and fix some hints 7a0c243 , which already have been merged. mypy is still unhappy with a lot of things.

I have been experimenting with typeguard, which turns those type-hints into runtime checks. This allows validating those hints when running the test-suite. Sadly that drastically increases the runtime and – even more sad – shows several errors in my type annotations. I have locally converted the unit-test to use pytest instead of Pythons built-in unittest as that allowed me to easily get coverage reports, but that also needs more work.

pmhahn avatar Sep 08 '25 11:09 pmhahn

I have a hunch this will never see the light of day. Would it be feasible to scale back the mission - type-annotate the user facing part of the API and mark the private stuff as off limits for the type checker?

sevaa avatar Nov 25 '25 16:11 sevaa

Yeah this should just be merged IMO. It's pretty much impossible to take an untyped Python project and add correct type hints to it all in one go. Once this is merged you can gradually fix the errors until it all type checks, and then enable type checking with Pyright in CI (or Pyrefly/Ty maybe by the time that actually happens!).

Timmmm avatar Nov 25 '25 18:11 Timmmm