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

Add OuputVerbosity and use it in assertions #11473

Merged
merged 30 commits into from
Nov 19, 2023

Conversation

plannigan
Copy link
Contributor

OuputVerbosity is a new class that allows for retrieving a verbosity level for a specific output type. If nothing is specifically configured, the application wide verbose value is used.

The first use of this functionality is verbosity_assertions which can be used to make the assertion reports output at the most verbose level while still showing a single character for each test case when progress is being reported.

Addresses #11387

return f"verbosity_{output_type}"

@staticmethod
def add_ini(parser: "Parser", output_type: str, help: str) -> None:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since many things will add these type of settings, I thought it would be helpful to have this function to ensure it is dong consistently. I'm not completely in love with this level of indirection. But it is necessary since config settings are added to the Parser before the Config object exists.

Copy link
Member

Choose a reason for hiding this comment

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

Good idea. 👍

class Config:
def getoption(self, name):
if name == "verbose":
def mock_config(verbose: int = 0, assertion_override: Optional[int] = None):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

With the move away from getoption(), this needed to get a bit more complicated. (In the long term, it would probably be beneficial move the code base to a consistent way to access the verbosity level.)

@plannigan
Copy link
Contributor Author

Since this is my first contribution, I'm not sure the best way to get a review for this PR that has been up for 3 weeks. @nicoddemus & @Zac-HD, you both interacted with the issue. Would one of you be able to review this code?

@nicoddemus
Copy link
Member

nicoddemus commented Oct 21, 2023

Since this is my first contribution, I'm not sure the best way to get a review for this PR that has been up for 3 weeks

Sorry @plannigan, you are not supposed to do anything else other than opening the PR: we dropped the ball for not reviewing it earlier (I personally have been busy). When that happens a ping (like you did) is appreciated, thank you!

I will try my best to review this in the next couple of days. 👍

Copy link
Member

@nicoddemus nicoddemus left a comment

Choose a reason for hiding this comment

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

Great work @plannigan, and sorry again for the delay!

The implementation is good, however I think we might need some more interactions given this introduce a new API, which must be done carefully and with care.

doc/en/how-to/output.rst Outdated Show resolved Hide resolved
doc/en/how-to/output.rst Show resolved Hide resolved
src/_pytest/assertion/__init__.py Outdated Show resolved Hide resolved
return f"verbosity_{output_type}"

@staticmethod
def add_ini(parser: "Parser", output_type: str, help: str) -> None:
Copy link
Member

Choose a reason for hiding this comment

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

Good idea. 👍

"""Application wide verbosity level."""
return cast(int, self._config.option.verbose)

def verbosity_for(self, output_type: str) -> int:
Copy link
Member

Choose a reason for hiding this comment

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

I think if one passes an invalid output_type at the moment, we will get an error (due to self._config.getini).

We probably want to have this classs accessible to plugins and users, via config.output_verbosity, so we need to think about its API carefully as we will need to support this officially moving on.

Some points:

  1. In tandem with pytest's direction regarding type safety, I think we should use an Enum with the output types currently available, something along the lines of:

    class VerbosityType(Enum):
        """..."""     
    
        Global = "global"
        Assertions = "assertions"

    Instead of a simple output_type: str parameter/value.

  2. The class name is OutputVerbosity and the public methods are verbosity and verbosity_for, which read a bit redundant: confg.output_verbosity.verbosity, confg.output_verbosity.get_verbosity_for. I suggest we use simply get, defaulting to the Global verbosity:

    def get(self, type: VerbosityType = VerbosityType.Global) -> int:
        ...

    Which reads well I think:

    config.output_verbosity.get()
    config.output_verbosity.get(VerbosityType.Assertions)
  3. We need to properly document both OutputVerbosity and VerbosityType in the reference docs (including some usage examples, but possibly we can write those in the class docstring). We also need to expose VerbosityType in the pytest namespace.

  4. Given OutputVerbosity is public, we should make OutputVerbosity.addini private, as it is intended for internal use only.

I'm not married to the names above, so suggestions and bikeshedding are welcome.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. I opted for str because I was thinking that plugins might want also register types, like any other config setting. Which would mean that it wouldn't be possible to statically define all of the types. However, reflecting on that, I'm not sure that level of extensibility is necessary. It is likely better for pytest to say "here are N buckets that a plugin can utilize" and that way users of multiple plugins that leverage this functionality will see consistent behavior without needing to enable M settings.
  2. That is fair. It also works nicely with the enum suggestion.
  3. Sure
  4. I had it as public for the reasons described in 1, but it makes sense to make it private based on that change.

changelog/11387.feature.rst Outdated Show resolved Hide resolved
Copy link
Member

@bluetech bluetech left a comment

Choose a reason for hiding this comment

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

I'm not able to do a full review ATM, but left some quick comments.

doc/en/how-to/output.rst Outdated Show resolved Hide resolved
Fine gain verbosity
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

In addition to specifying the application wide verbosity level, it is possible to control specific aspects independently.
Copy link
Member

Choose a reason for hiding this comment

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

Since we only have one aspect at the moment, it seems premature to write the prose in a general/list format.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My intention is to implement some of the other verbosity types I identified once this PR is merged. A different suggestion was to add a note that only one is currently supported. That seems sufficient to me if "life happens" and it takes me some time to get to adding other types.

doc/en/how-to/output.rst Outdated Show resolved Hide resolved
doc/en/reference/reference.rst Show resolved Hide resolved
src/_pytest/assertion/truncate.py Outdated Show resolved Hide resolved
@@ -1662,6 +1663,54 @@ def _warn_about_skipped_plugins(self) -> None:
)


class OutputVerbosity:
Copy link
Member

Choose a reason for hiding this comment

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

Why OutputVerbosity and not just Verbosity? I can't think of a verbosity that is not related to output, so it seems redundant.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm open tweaking the name. I generally default to more explicit names.

src/_pytest/config/__init__.py Outdated Show resolved Hide resolved
src/_pytest/config/__init__.py Outdated Show resolved Hide resolved
src/_pytest/config/__init__.py Outdated Show resolved Hide resolved
plannigan and others added 6 commits October 31, 2023 14:03
Co-authored-by: Ran Benita <ran@unusedvar.com>
Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
* Use enum instead of string
* Change method name to simple get
* Make type registering internal
@plannigan
Copy link
Contributor Author

I believe I have addressed the technical comments and documentation requests.

I'm not sure why the doc building step is failing. The error don't seem to be related to the lines I changed. If someone with more experience with sphinx can take a look, I would appreciate it.

@nicoddemus
Copy link
Member

I'm not sure why the doc building step is failing

The problem was that you used ^^^ for the heading in your new session, while in the rest of the document we used ~~~ for that level of heading. I pushed a fix. 👍

Copy link
Member

@bluetech bluetech left a comment

Choose a reason for hiding this comment

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

Thanks for the update @plannigan, my comments:

  1. The OutputVerbosity and VerbosityType seem more complicated than necessary to me. What is the advantage over doing it the direct way, i.e. adding the ini directly and maybe adding a getter on Config?

  2. If we stick with OutputVerbosity, let's make it just Verbosity -- the Output part is entirely redundant IMO.

  3. We should have at least one end-to-end test (pytester.runpytest test) which shows this working, i.e. changing the output vs. not setting the ini.

@nicoddemus
Copy link
Member

i.e. adding the ini directly

I think this is to handle the fallback to default verbosity when the setting is defined in the ini file.

and maybe adding a getter on Config?

Can you clarify what you mean here, do you mean adding:

  1. Config.get_verbosity(VerbosityType) -> int
  2. Config.get_verbosity(Literal["assertion"]) -> int
  3. Config.get_assertion_verbosity() -> int

@bluetech
Copy link
Member

bluetech commented Nov 8, 2023

Can you clarify what you mean here, do you mean adding:

Yes, any of these would be fine by me.

@nicoddemus
Copy link
Member

nicoddemus commented Nov 8, 2023

One nice effect of implementing some functionality directly in Config as suggested by @bluetech is that it would cut down on the number of things exposed publicly, specially 2) in my suggestion above.

Using Config.get_verbosity(str) we can by default return the default verbosity if an unknown verbosity is passed , to make the life of plugin authors easier: using a VerbosityType enum has many advantages, but one disadvantage is that plugin authors that need to support multiple pytest versions need to do some juggling to see if they have access to VerbosityType.X.

Suppose in the future we add VerbosityType.Rubles in pytest 9.0, plugins would have to juggle a bit to support pytest 7.x+ up to 9:

if hasattr(config, "output_verbosity") and hasattr(pytest, "VerbosityType") and hasattr(pytest.VerbosityType, "Rubles"):
    rubles_verbosity = config.output_verbosity.get(pytest.VerbosityType, Rubles)
else:
    rubles_verbosity = config.option.verbose

While using option 2) above users can go with:

if hasattr(config, "get_verbosity"):
    rubles_verbosity = config.get_verbosity("rubles")
else:
    rubles_verbosity = config.option.verbose

changelog/11387.feature.rst Outdated Show resolved Hide resolved
testing/test_assertion.py Show resolved Hide resolved
@plannigan
Copy link
Contributor Author

Looks like one of the jobs failed with a file system error in an area I didn't touch. I don't have permissions to retry the job.

@nicoddemus
Copy link
Member

Looks like one of the jobs failed with a file system error in an area I didn't touch. I don't have permissions to retry the job.

Started it again. 👍

Copy link
Member

@bluetech bluetech left a comment

Choose a reason for hiding this comment

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

Haven't had time for a full review again, but left some comments, please take a look.

src/_pytest/config/__init__.py Show resolved Hide resolved
src/_pytest/config/__init__.py Show resolved Hide resolved
src/_pytest/config/__init__.py Show resolved Hide resolved
return f"verbosity_{verbosity_type}"

@staticmethod
def _add_ini(parser: "Parser", verbosity_type: str, help: str) -> None:
Copy link
Member

Choose a reason for hiding this comment

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

Same here, the name _add_ini is too general now.

Also, IMO this function doesn't add much value, I think it would be better to reduce the indirection and inline it. But since it's private then it's OK if you want to keep it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I prefer to keep it because it will avoid duplication as more types are added. It also ensures that the same default is used across verbosity types. If someone adds an Nth type, but uses "global", then get_verbosity() will raise an exception.

src/_pytest/config/__init__.py Show resolved Hide resolved
testing/test_assertion.py Outdated Show resolved Hide resolved
testing/test_assertion.py Outdated Show resolved Hide resolved
Copy link
Member

@bluetech bluetech left a comment

Choose a reason for hiding this comment

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

Thanks for the update @plannigan, please see my comment on _KNOWN_VERBOSITY_TYPES. Other than that it looks good!

doc/en/reference/reference.rst Outdated Show resolved Hide resolved
src/_pytest/assertion/truncate.py Show resolved Hide resolved
assert isinstance(global_level, int)
if (
verbosity_type is None
or verbosity_type not in Config._KNOWN_VERBOSITY_TYPES
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be better to avoid this check, so that plugins can add their own verbosity types. We would instead check if the ini exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like the idea of checking the ini data instead (one less thing to manually manage).

In a previous conversation, there was a conversation about plugins creating their own levels. I think I'm still in the "pytest can enumerate a few buckets" camp. @nicoddemus Do you have any thoughts? If we did allow plugins to create types, _add_verbosity_ini would have to be made public and some more documentation updates.

Copy link
Member

Choose a reason for hiding this comment

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

I think we can keep it simple for now, making this internal only; if the need comes later, we can discuss how to make this public -- better to err on the side of caution, as it is easy to make something public later, rather than making something public and then regretting it later.

src/_pytest/config/__init__.py Outdated Show resolved Hide resolved
Copy link
Member

@bluetech bluetech left a comment

Choose a reason for hiding this comment

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

LGTM! I know this feature will make a lot of people happy :)

I'll let @nicoddemus do the final review/merge.

@nicoddemus
Copy link
Member

Thanks everyone!

@nicoddemus nicoddemus merged commit 9dc1fc4 into pytest-dev:main Nov 19, 2023
24 checks passed
@plannigan plannigan deleted the fine-grain-verbosity branch November 25, 2023 20:15
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.

None yet

3 participants