Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable mypy for ngclient #1489

Merged
merged 9 commits into from
Aug 30, 2021
Merged

Conversation

sechkova
Copy link
Contributor

@sechkova sechkova commented Jul 9, 2021

Fixes #1409
Description of the changes being introduced by the pull request:

Enable static type checking for ngclient.

  • add all missing type annotations
  • fix errors (partially for updater.py and trusted_metadata_set.py
  • add urllib3 to ignored includes
  • download suggested stub packages when running mypy

Please verify and check that the pull request fulfills the following
requirements
:

  • The code follows the Code Style Guidelines
  • Tests have been added for the bug fix or new feature
  • Docs have been added for the bug fix or new feature

@jku
Copy link
Member

jku commented Jul 12, 2021

I think there's a few cases here:

  • TrustedMetadataSet.root probably does not actually need to be Optional: Unlike other properties it's guaranteed to be a Metadata after constructor, right?
  • mypy gets confused about target_base_url: I'm sure it's possible to simplify the code so mypy understands that target_base_url cannot be None
  • cases where we know a e.g. self._trusted_set.snapshot is Metadata but code does not: I think this is a great place for assert(self._trusted_set.snapshot is not None)
  • many cases of Metadata.Signed typing problem...
    • I believe this would get fixed with Improve signed typing #1457 and Generics
    • alternatively we could just use calls like assert(isinstance(self._trusted_set.snapshot.signed, Snapshot)). Ugly but works and there are surprisingly few places where this is needed (like 10-12 or so?)
    • even more alternatively we could add properties to TrustedMetadataSet that returned Root, Snapshot, Timestamp and Targets (but that makes a lot of the code a bit ugly as you usually need both the Metadata and the Signed subclass)...

First two-three can be fixed now. The last one we can discuss... In any case this is useful work as it shows how much (or little) the lack of signed typing affects us...

The download related the changes should maybe only be done after the cleanup is done in #1448 (also the SlowRetrievalError change looks a bit fishy -- I would expect it's used in the legacy code as well?)

@jku jku mentioned this pull request Jul 12, 2021
2 tasks
@joshuagl
Copy link
Member

joshuagl commented Jul 12, 2021

  • mypy gets confused about target_base_url: I'm sure it's possible to simplify the code so mypy understands that target_base_url cannot be None

It's debatable whether this simplifies things for humans reading the code (edit: though, apart from the variable name ugliness and possibility for confusion of tgt_base_url, I think it probably is simpler) but I was able to make mypy happy with the following:

        # locally scoped variable that is definitely a string, even if empty
        tgt_base_url: str = self._target_base_url or ""
        if target_base_url is not None:
            tgt_base_url = _ensure_trailing_slash(target_base_url)

        # move the check for a base URL being set to after the locally scoped
        # variable assignment logic is bottomed out.
        if not tgt_base_url:
            raise ValueError(
                "target_base_url must be set in either download_target() or "
                "constructor"
            )

see the full diff here.

@jku
Copy link
Member

jku commented Jul 12, 2021

this should also work -- the issue for mypy is two branches that seem unrelated (even if they aren't) so making the branches actually same should make the intent clear to computer...

        if target_base_url is None:
            if self._target_base_url is None:
                raise ValueError(
                    "target_base_url must be set in either download_target() or "
                    "constructor"
                )
            target_base_url = self._target_base_url
        else:
            target_base_url = _ensure_trailing_slash(target_base_url)

@jku
Copy link
Member

jku commented Jul 14, 2021

The download related the changes should maybe only be done after the cleanup is done in #1448 (also the SlowRetrievalError change looks a bit fishy -- I would expect it's used in the legacy code as well?)

#1448 is now merged

@sechkova
Copy link
Contributor Author

sechkova commented Jul 20, 2021

Rebased on latest changes in develop and fixed all issues but the Metadata.Signed typing ones.

@sechkova
Copy link
Contributor Author

Actually bandit complains slightly about asserts:

>> Issue: [B101:assert_used] Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
   Severity: Low   Confidence: High
   Location: tuf/ngclient/updater.py:297
   More Info: https://bandit.readthedocs.io/en/latest/plugins/b101_assert_used.html
296	
297	            assert self._trusted_set.snapshot is not None
298	            metainfo = self._trusted_set.snapshot.signed.meta[f"{role}.json"]

@joshuagl
Copy link
Member

Actually bandit complains slightly about asserts

Right. We could reasonably silence those warnings, as we are only suggesting use of asserts to help mypy determine the expected type. But I think this further reinforces the 'code smell' of using asserts to solve this problem.

@sechkova
Copy link
Contributor Author

Actually bandit complains slightly about asserts

Right. We could reasonably silence those warnings, as we are only suggesting use of asserts to help mypy determine the expected type. But I think this further reinforces the 'code smell' of using asserts to solve this problem.

In this case we use assert as the recommended way to deal with None errors. So we either silence bandit or tweak the TrustedMetadataSet public properties to not return optional values.

@sechkova sechkova force-pushed the enable-mypy-ngclient branch 2 times, most recently from 1b951fb to 871f68c Compare July 28, 2021 14:52
@sechkova sechkova changed the title WIP: Enable mypy for ngclient Enable mypy for ngclient Jul 28, 2021
@sechkova sechkova marked this pull request as ready for review July 28, 2021 14:54
@sechkova
Copy link
Contributor Author

I used the part of #1457 that makes Metadata 'Generic' to resolve the type checking issues related to the 'signed' type.
Now there are no more mypy errors left and the PR should be ready for review.

Copy link
Member

@joshuagl joshuagl left a comment

Choose a reason for hiding this comment

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

Thanks Teodora. This is looking good. I have a minor comment on the change to the root property, and a slight pushback on the type hinting for IDE's that I would appreciate your thoughts on before we merge.

tuf/api/metadata.py Show resolved Hide resolved
tuf/ngclient/updater.py Show resolved Hide resolved
tuf/ngclient/_internal/trusted_metadata_set.py Outdated Show resolved Hide resolved
tuf/ngclient/updater.py Outdated Show resolved Hide resolved
tuf/ngclient/_internal/trusted_metadata_set.py Outdated Show resolved Hide resolved
tuf/ngclient/_internal/trusted_metadata_set.py Outdated Show resolved Hide resolved
@sechkova
Copy link
Contributor Author

Seems like the CI won't be triggered until I rebase and resolve the conflicts.

@sechkova sechkova force-pushed the enable-mypy-ngclient branch 2 times, most recently from 4dc28d6 to 8c172af Compare July 29, 2021 14:58
@jku
Copy link
Member

jku commented Aug 5, 2021

  • add urllib3 to ignored includes

If you researched this can you document why? looking in urllib3 git the annotations seem to be there but is that just for some future 2.0 release?

  • download suggested stub packages when running mypy

Could we add the required stub packages to test requirements instead?

Copy link
Member

@jku jku left a comment

Choose a reason for hiding this comment

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

I think it looks good (but obviously the last commits depend on a commit from generics PR #1457 so I'm not approving).

This PR isn't using the whole Generics branch though: if it did then it could just remove all of the checks of type if new_root.signed.type != "root" since the constructor would do that already

The last commit is IMO not great since it has so much code in the try-block: it's not clear that the KeyError should only happen in self.root... I think the original code is better. Maybe the real alternative would be to not call update_root() from init but instead do the required steps in init itself -- that seems ok to me as well as I think there aren't that many steps here.

tuf/ngclient/_internal/trusted_metadata_set.py Outdated Show resolved Hide resolved
tuf/ngclient/_internal/requests_fetcher.py Outdated Show resolved Hide resolved
@sechkova
Copy link
Contributor Author

sechkova commented Aug 6, 2021

I think it looks good (but obviously the last commits depend on a commit from generics PR #1457 so I'm not approving).

Hmm ... I wouldn't say directly depending, I didn't cherry-pick the commit from the other PR but created a new one and added you as a co-author.

This PR isn't using the whole Generics branch though: if it did then it could just remove all of the checks of type if new_root.signed.type != "root" since the constructor would do that already

This was done intentionally but I guess I should've tried to explain my intentions somewhere in written form.

The "Generics" branch tries to achieve two things simultaneously: correct type annotations of Signed and a runtime check ensuring content and type are really what we expect. Since there seems to be an agreement already on the use of Generics and an ongoing discussion around the runtime check part, I decided to include only the first part here, the one directly related to type annotations and mypy. This way we can leave the rest of the discussion for #1457 which will also make it simpler because it will try to solve only one problem. I hope you agree :)

@sechkova
Copy link
Contributor Author

sechkova commented Aug 6, 2021

The last commit is IMO not great since it has so much code in the try-block: it's not clear that the KeyError should only happen in self.root... I think the original code is better. Maybe the real alternative would be to not call update_root() from init but instead do the required steps in init itself -- that seems ok to me as well as I think there aren't that many steps here.

I tried this alternative since @joshuagl expressed some concerns about not using the 'root' property consistently inside the class. The three options, none of them perfect, would be:

  • don't call update_root from init, repeat some of the code (not than much repetition so could be acceptable)
  • don't use the property but "get" safely from self._trusted_set c205ac0 (concerns about not using consistently the root property)
  • use the root property, enclose it in a try bock and catch the KeyError 8c172af (well you already described the disadvantages)

There is a forth one where we keep root optional as the rest of the properties and add asserts in the code but I tried and this means asserts at 4-5 places which seems to rule out this option.

@jku
Copy link
Member

jku commented Aug 9, 2021

I think it looks good (but obviously the last commits depend on a commit from generics PR #1457 so I'm not approving).

Hmm ... I wouldn't say directly depending, I didn't cherry-pick the commit from the other PR but created a new one and added you as a co-author.

This PR isn't using the whole Generics branch though: if it did then it could just remove all of the checks of type if new_root.signed.type != "root" since the constructor would do that already

This was done intentionally but I guess I should've tried to explain my intentions somewhere in written form.

Yeah I think the generics change is major enough that we can't just hide it in a PR titled "enable mypy for ngclient" :) We can definitely split the PR if that makes it easier to swallow though.

@joshuagl
Copy link
Member

Yeah I think the generics change is major enough that we can't just hide it in a PR titled "enable mypy for ngclient" :) We can definitely split the PR if that makes it easier to swallow though.

Ack, let's do it.

@sechkova
Copy link
Contributor Author

  • add urllib3 to ignored includes

If you researched this can you document why? looking in urllib3 git the annotations seem to be there but is that just for some future 2.0 release?

Yes, the latest release is 1.26 which does not include the type annotations. I updated the commit description.

  • download suggested stub packages when running mypy

Could we add the required stub packages to test requirements instead?

Done: d306370

@jku
Copy link
Member

jku commented Aug 16, 2021

Ok, I've made the #1457 PR into a "minimal generics" PR -- this PR could be based off of that one I believe.

@sechkova
Copy link
Contributor Author

Rebased on #1457 and I believe I have addressed all comments so waiting for another review.

@sechkova sechkova requested a review from jku August 26, 2021 13:52
Copy link
Member

@jku jku left a comment

Choose a reason for hiding this comment

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

Looks good, updater.py in VSCode is much nicer now. I think this is good to go... except this will definitely clash with #1514 which I was about to merge...

@@ -348,6 +345,7 @@ def _load_snapshot(self) -> None:
# Local snapshot does not exist or is invalid: update from remote
logger.debug("Failed to load local snapshot %s", e)

assert self._trusted_set.timestamp is not None # nosec
Copy link
Member

@jku jku Aug 27, 2021

Choose a reason for hiding this comment

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

this is unfortunate... I don't have any better ideas but it looks so strange in this code.

This seems to only be needed in Updater for getting meta-info -- which is also a sort of duplicate piece of code here and in _load_targets()... so there's probably some sort of refactoring possibility here but let's not do that in this PR.

tuf/api/metadata.py Show resolved Hide resolved
Extend mypy to include all files under ngclient.

Signed-off-by: Teodora Sechkova <tsechkova@vmware.com>
Add the stub for the requests package (types-requests)
to requirements-tests.txt.

Add urllib3 to the ignored imports. The project seems
to have added type annotations already but has not
released a version including them yet.

Signed-off-by: Teodora Sechkova <tsechkova@vmware.com>
Needed in order to be compatible with the return type of
download_file (TemporaryFile is typed as IO[bytes]).
BinaryIO is a subclass of IO[bytes].

Signed-off-by: Teodora Sechkova <tsechkova@vmware.com>
Add missing annotations and partially resolve mypy
errors in updater.py and trusted_metadata_set.py

Signed-off-by: Teodora Sechkova <tsechkova@vmware.com>
This is done only for hinting 'mypy' that we have
ensured these values cannot be None.

'Bandit' raises warnings for assert usage in the code
but we are disabling them since we do not rely upon
'asserts' for any runtime checks but only for type checking.

Signed-off-by: Teodora Sechkova <tsechkova@vmware.com>
The 'root' property is guaranteed to be set after init.

Signed-off-by: Teodora Sechkova <tsechkova@vmware.com>
SlowRetrievalError is raised from RequestsFetcher where
average_download_speed is not calculated.

Signed-off-by: Teodora Sechkova <tsechkova@vmware.com>
By explicitly denoting the expected type of Metadata.signed
we help mypy understand our intentions and correctly figure
out types. This is entirely a typing feature and has no
runtime effect.

Modify the return type of Metadata.from_dict to match the
other factory methods (from_*).

Signed-off-by: Teodora Sechkova <tsechkova@vmware.com>
Add an additional private method for loading the initial
trusted root metadata. The public method update_root() is
now used only externally for updating the intiial root.
The 'root' property is used only after its initialization
in the constructor and is not longer optional which makes
mypy happy.

This split results in cleaner code and the ability to annotate
the 'root' property as non-optional at the cost of some code
duplication.

Signed-off-by: Teodora Sechkova <tsechkova@vmware.com>
@sechkova
Copy link
Contributor Author

Rebased on latest changes in develop.

@jku jku merged commit 3028fb6 into theupdateframework:develop Aug 30, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ngclient: type annotations review and mypy integration
3 participants