-
Notifications
You must be signed in to change notification settings - Fork 13
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
Reach 100% code coverage #13
Conversation
Also enable pip caching on CI
…p pypy becasue it's much faster.
This helps with our coverage numbers. Under Python 2, we also start using the C implementation, and avoid some unnecessary encode/decode cycles, so we might be a tiny bit faster.
The str values of the exceptions.
Also when it raises an error, preserve the traceback if we're re-raising. This was invaluable in debugging this.
Had to use one 'no cover' sadly.
…entedError. This helps coverage numbers.
Notable: - float conversion now correctly handles bytes and unicode prohibited values on all versions. - remove a duplicate test function from test_logger. - remove test support in test_logger for < 2.7
- Fix StartupHandler on Python 3.
- SectionType has a few no-covers for exception cases that I didn't feel like mocking up all the conditions for. - Make the type objects, well, objects. They had a ``__metaclass__`` statement that I think did the same thing on Python 2.
There are unfortunately a few no-covers because of very deeply nested functions that are difficult to mock around. There is at least one genuine bug fix.
…ecause I couldn't come up with counter examples.
Changes Unknown when pulling cc07327 on coverage-100 into ** on master**. |
@jamadden Now that it is at 100%, can you make coveralls not spew into the conversation itself? I hate getting emails from bots. :) |
I sure can and did. It's on by default, and the badge sure is pretty to look at, but it probably doesn't add much value. And there was that recent bug where it was commenting for every Travis build in the matrix which was really annoying; fortunately that seems to have been fixed. |
Making the tests fail when coverage drops below 100% (as @tseaver likes to do, which I greatly admire) is a better way to maintain test coverage than those Coveralls comments that annoy everyone and that nobody ever reads. |
PRs are set to fail in that case. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM with minor comments.
.coveragerc
Outdated
# things. Omit the main methods of test files. | ||
exclude_lines = | ||
pragma: no cover | ||
pragma no cover |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very minor but it bugs me that we have two ways of spelling that pragma.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess there are no occurrences of 'pragma no cover' anymore so that can go away.
|
||
exec_("""def reraise(tp, value, tb=None): | ||
raise tp, value, tb | ||
""") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kind of hate the way this is split into multiple lines. I'd prefer
exec_("""
def reraise(tp, value, tb=None):
raise tp, value, tb
""")
or
exec_("def reraise(tp, value, tb=None): raise tp, value, tb")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For better or worse, that code is copied verbatim from six
and various other zopefoundation packages that have done the same.
ZConfig/cfgparser.py
Outdated
@@ -175,7 +170,8 @@ def replace(self, text): | |||
raise | |||
|
|||
def error(self, message): | |||
raise ZConfig.ConfigurationSyntaxError(message, self.url, self.lineno) | |||
v = ZConfig.ConfigurationSyntaxError(message, self.url, self.lineno) | |||
reraise(type(v), v, sys.exc_info()[2]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand the purpose of this change. Are you replacing the exception class and message but keeping the original traceback?
Perhaps define a helper function raise_with_same_tb(exc)
, because I see a lot of places in the diff that do this sort of thing with new temporary variables (all named differently) and type(...)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is to keep the original traceback. There are lots of places in the code that do try:...except: self.error()
. Debugging those was a nightmare without keeping the original traceback.
The simplest textual change was to change the (relatively few) error
functions instead of changing all the callers.
if is_str: | ||
if v.lower() in ["inf", "-inf", "nan"]: | ||
if v.lower() in (u"inf", u"-inf", u"nan", b'inf', b'-inf', b'nan'): | ||
raise ValueError(repr(v) + " is not a portable float representation") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
float()
was fixed in Python 2.6 to interpret inf
and nan
in a portable fashion, so this check can be safely removed as ZConfig no longer needs to protect the users from this pitfall.
That would basically simplify this whole function into return float(v)
, probably making it unnecessary -- just use float()
wherever float_conversion()
is currently being called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not intimately familiar with the history there, but the linked docs still contain the phrase "When passing in a string, values for NaN and Infinity may be returned, depending on the underlying C library" (emphasis mine). It does say that float represents things a certain way, but it's not clear to me that it commits to parsing things a certain way.
I'd be happy to see this simplified, and that could arguably be a part of the changes for this PR. It's just not entirely clear to me its safe to do so.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "depending on the underlying C library" bit remains from the earlier text, which warned that the specific inf/nan spellings did depend on the C library: http://svn.python.org/view/python/trunk/Doc/library/functions.rst?r1=59558&r2=59557&pathrev=59558. I wonder why it wasn't removed.
The "Float accepts the strings nan, inf and -inf for NaN and positive or negative infinity. The case and a leading + are ignored as well as a leading - is ignored for NaN." part is about the parsing. There are stdlib tests for this too.
The "Float always represents NaN and infinity as nan, inf or -inf." part is about the repr()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, cool! Thanks for the pointers. Should this change be a part of this admittedly large PR, or should it wait? ( I am willing to create follow up issues for this and other things that come out of this discussion. )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Separate PR, I think, to keep things simple.
@@ -228,6 +220,8 @@ def __init__(self, name, sectiontype, minOccurs, maxOccurs, handler, | |||
# handler - handler name called when value(s) must take effect, | |||
# or None | |||
# attribute - name of the attribute on the SectionValue object | |||
assert maxOccurs is not None | |||
# because we compare it to 1 which is a Py3 error |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is strange then that the constructor explicitly allows maxOccurs
to be set to None
, and that, e.g., def ismulti
is comparing self.maxOccurs > 1
unconditionally.
Python 2.x returns False for None > 1
. I cannot say whether that makes sense for ismulti
or here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BTW getdefault
also unconditionally compares self.maxOccurs > 1
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the weirdness is partially because this is a subclass of an abstract class that allows subclasses to define the constructors differently (which they do, passing constants for some of the arguments). That comparison works on Py2, but fails on Py3.
Once we do the assert in the constructor, getdefault doesn't matter anymore?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think we should either fully support maxOccurs
being None
(maybe that is how you spell "unlimited"?) by adding code to make Python 3 handle this explicitly instead of failing, or we should require it to be not-None everywhere (i.e. remove the explicit "let it be None" case in the constructor and replace it with an assertion, or, better yet, an explicit ValueError).
I'm sorry that I don't currently have the time to investigate what maxOccurs
is used for!
ZConfig/tests/test_config.py
Outdated
'malformed section start', | ||
self.loadtext, '<section') | ||
|
||
# ConfigLoader.endSection raises this and its recaught and changed to a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: its -> it's
@@ -93,14 +93,13 @@ def test_datatype_float(self): | |||
|
|||
# These are not portable representations; make sure they are | |||
# disallowed everywhere for consistency. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I mentioned above, this comment is no longer true since Python 2.6.
check(":80", AF_INET, (defhost, 80)) | ||
|
||
convert = self.types.get('socket-connection-address') | ||
check(":80", AF_INET, ("127.0.0.1", 80)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is a strange amount of blank spaces between function arguments. PEP-8 frowns on this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed. But that matches the way this is called in the rest of the function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can only imagine I was thinking that looking like a table would make this more readable. I wouldn't make the same mistake now. (I certainly hope.)
suite.addTest(unittest.makeSuite(AbstractTypeTestCase)) | ||
suite.addTest(unittest.makeSuite(SectionTypeTestCase)) | ||
suite.addTest(unittest.makeSuite(SchemaTypeTestCase)) | ||
return suite |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strongly suggest
def test_suite()
return unittest.defaultTestLoader.loadTestsFromName(__name__)
unless you have abstract classes inheriting from unittest.TestCase
that would preclude this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't seen that idiom used anywhere in this codebase (or in any of the other zopefoundation projects I've touched so far). In fact, I wasn't actually aware of it.
I used this style because that's what was used in the rest of the code base. I'd be happy to see it change to a simpler style, but I submit that would belong in a separate PR that does the same for all the test modules.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't recall where I learned this trick, but I recall getting burnt a few times in Zopeland when I (or somebody else) wrote a new test class and then forgot the corresponding makeSuite()
bit. Of course, measuring test coverage protects from that to some extent.
I also have doubts whether that refactoring belongs to this already-very-large PR (especially if we want to convert all the files in one go, rather opt for a more incremental approach and convert just those that are touched in this PR).
I have this habit where when I'm reviewing code I also tend to look for possible improvements in the surrounding codebase, and also for opportunities to share knowledge. But then I forget to state that that's what I'm doing, rather than asking for changes in this particular PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was burnt a ridiculous number of times on this very PR by that issue :/ it was quickly noticed and fixed, but still.
I'm happy to create a follow up issue. And I will be doing the same for other projects where I've dealt with this (relstorage).
return doctest.DocFileSuite("schemaless.txt", package="ZConfig") | ||
suite = unittest.makeSuite(TestSection) | ||
suite.addTest(doctest.DocFileSuite("schemaless.txt", package="ZConfig")) | ||
return suite |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you switch to using defaultTestLoader
everywhere, a nice way to mix it with doctests is
def test_suite():
return unittest.TestSuite([
unittest.defaultTestLoader.loadTestsFromName(__name__),
doctest.DocFileSuite(...),
])
ZConfig/_compat.py
Outdated
# Python 2 | ||
from io import BytesIO as _NStringIO | ||
|
||
NStringIO = _NStringIO |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the alias?
# Python 3 support. | ||
import urllib.request as urllib2 | ||
|
||
urllib2 = urllib2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why rebind the name?
@@ -155,7 +157,7 @@ def get_section_info(self, type, name): | |||
bk = self.basic_key(s, pos) | |||
if name and self._normalize_case(s) == name: | |||
L.append((optpath[1:], val, pos)) | |||
elif bk == type: | |||
elif bk == type: # pragma: no cover |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh, the shadowing of the type
built-in is really ugly: I spent more than a bit of time trying to work out how the code worked. I wonder if we could rename the argument type_
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There would be a lot or renames to do throughout the codebase. type
(among others) is commonly shadowed; I think this code predates new-style classes and type
as a builtin.
I'd be happy to see that done, but I submit that it belongs in another PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type
has been a built-in for a very long time (since before I discovered Python at version 1.2). I've never been particularly averse to shadowing it, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW when I was reviewing this, I wanted to ask why this comparison wasn't using if bk is type
to compare with the type
builtin. Then I thought a bit more and decided maybe type
was a local variable shadowing the builtin.
(Also, given that my Vim syntax-highlights type
in a different color from regular variables makes me want to use type_
instead of shadowing.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mgedmin that was exactly what I thought when reading that diff (it took me a couple of minutes to stumble on the shadowing).
SysLogHandler = SysLogHandler | ||
HTTPHandler = HTTPHandler | ||
SMTPHandler = SMTPHandler | ||
Win32EventLogHandler = Win32EventLogHandler |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand what "export these" means, or what that has to do with re-binding the name.
On another note: couldn't handlers.py
just import them from logging.handlers
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This applies to this comment and two of the previous ones:
linters like flake8 and pylint (I run/ran both during this process) complain (rightfully) about unused imports. The way to fix that without adding #noqa
or what have you is to use the import, and the simplest way to do that is to rebind it to the same name. This declarative statement seems reasonably clear to me that this name is a public part of the module definition, as opposed to a side-effect of the import.
In this particular case, it seemed important to leave the imports as they are for backwards compatibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's just hope linters won't decide to start warning about "useless assignment X = X" in the future ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- I find the reassignment useless, and would prefer a comment like
#noqa API
to make the intent clearer. - I don't think we need to support backward-compatibility for re-exports of stdlib modules.
except ImportError: | ||
# Python 3 support. | ||
import io as StringIO | ||
from ZConfig._compat import NStringIO as StringIO |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not leave the name NStringIO
, since we have to munge all the callers anyway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seemed like a slightly smaller change, but I admit it wasn't fully thought through. There was some evolution here. It can still change if desired.
I created new issues for the things that have come out in the comments. Is this mergeable? |
You still have a green check from me :) The only thing I still find kind of distasteful in the new changes is this:
|
Ah, now I understand what you mean by that. That should be easy enough to change and reduce the size of the PR a bit. I'll do that. |
Thank you, looks great! |
Remove 'try/except NameError' around uses of __file__ See #13 (comment)
Mostly this means new tests. Sometimes its means "no cover" markers. In a few cases it means using a shorter, more declarative way to write the code (e.g.,
@total_ordering
orABCMeta
) or unifying python2/python 3 branches as much as possible. Sometimes we got lucky and could drop code meant for 2.6 or earlier.There are a few genuine bugfixes in here that were revealed by getting the coverage up.
I tried to limit the use of 'no cover' as much as possible. There are a few deeply nested functions that made it difficult to avoid, though.
This replaces #12.