One explanation that seems plausible to me is that many programmers are actually having a hard time with formalization and logic rules (e.g., implication, quantifiers), maybe due to missing education (e.g. many programmers are people who came to programming from other less-formal fields). It's hence easier for them to write in human text and takes substantial cognitive load to formalize these thoughts in code. Does that explains it?
Hi Marko,
I think there are several ways to approach this problem, though am not weighing in on whether DbC is a good thing in Python. I wrote a simple implementation of DbC which is currently a run-time checker. You could, with the appropriate tooling, validate statically too (as with all approaches). In my approach, I use a “proxy” object to allow the contract code to be defined at function definition time. It does mean that some things are not as pretty as one would like - anything that cannot be hooked into with magic methods i.e isinstance, but I think this is acceptable as it makes features like old easier. Also, one hopes that it encourages simpler contract checks as a side-effect. Feel free to take a look - https://github.com/agoose77/pyffel
It is by no means well written, but a fun PoC nonetheless.
Regards,
Angus
To me, as I've said, DbC imposes a very large cost for both writers and readers of code.
packagery.resolve_initial_paths(initial_paths)Resolve the initial paths of the dependency graph by recursively adding *.py files beneath given directories.
| Parameters: | initial_paths ( |
|---|---|
| Return type: |
|
| Returns: | list of initial files (i.e. no directories) |
| Requires: |
|
| Ensures: |
|
>>> result = packagery.resolve_initial_paths([])
[]
>>> with temppathlib.NamedTemporaryFile() as tmp1, \
... temppathlib.NamedTemporaryFile() as tmp2:
... tmp1.path.write_text("some text")
... tmp2.path.write_text("another text")
... result = packagery.resolve_initial_paths([tmp1, tmp2])
... assert all(pth.is_absolute() for pth in result)
... assert all(pth.is_file() for pth in result)
>>> with temppathlib.TemporaryDirectory() as tmp:
... packagery.resolve_initial_paths([tmp.path])
Traceback (most recent call last):
...
ValueError("Unexpected directory in the paths")
>>> with temppathlib.TemporaryDirectory() as tmp:
... pth = tmp.path / "some-file.py"
... pth.write_text("some text")
... packagery.initial_paths([pth.relative_to(tmp.path)])
Traceback (most recent call last):
...
ValueError("Unexpected relative path in the initial paths")
To me, as I've said, DbC imposes a very large cost for both writers and readers of code.
@icontract.pre(lambda initial_paths: all(pth.is_absolute() for pth in initial_paths))
@icontract.post(lambda result: all(pth.is_file() for pth in result))
@icontract.post(lambda result: all(pth.is_absolute() for pth in result))
@icontract.post(lambda initial_paths, result: len(result) >= len(initial_paths) if initial_paths else result == [])
def resolve_initial_paths(initial_paths: List[pathlib.Path]) -> List[pathlib.Path]:
...
I'm not alone in this. A large majority of folks formally educated in computer science and related fields have been aware of DbC for decades but deliberately decided not to use them in their own code. Maybe you and Bertram Meyer are simple better than that 99% of programmers... Or maybe the benefit is not so self-evidently and compelling as it feels to you.
The vast majority of those developing software - even that intended to be reused - are simply ignorant of the concept. As a result they produce application programmer interfaces (APIs) that are under-specified thus passing the burden to the application programmer to discover by trial and error, the 'acceptable boundaries' of the software interface (undocumented contract's terms). But such ad-hoc operational definitions of software interface discovered through reverse-engineering are subject to change upon the next release and so offers no stable way to ensure software correctness.The fact that many people involved in writing software lack pertinent education (e.g., CS/CE degrees) and training (professional courses, read software engineering journals, attend conferences etc.) is not a reason they don't know about DBC since the concept is not covered adequately in such mediums anyway. That is, ignorance of DBC extends not just throughout practitioners but also throughout educators and many industry-experts.
The simplicity and obvious benefits of Design By Contract lead one to wonder why it has not become 'standard practice' in the software development industry. When the concept has been explained to various technical people (all non-programmers), they invariably agree that it is a sensible approach and some even express dismay that software components are not developed this way.(Emphasis mine; iContract refers to a Java design-by-contract library)It is just another indicator of the immaturity of the software development industry. The failure to produce high-quality products is also blatantly obvious from the non-warranty license agreement of commercial software. Yet consumers continue to buy software they suspect and even expect to be of poor quality. Both quality and lack-of-quality have a price tag, but the difference is in who pays and when. As long as companies can continue to experience rising profits while selling poor-quality products, what incentive is there to change? Perhaps the fall-out of the "Year 2000" problem will focus enough external pressure on the industry to jolt it towards improved software development methods. There is talk of certifying programmers like other professionals. If and when that occurs, the benefits of Design By Contract just might begin to be appreciated.
But it is doubtful. Considering the typical 20 year rule for adopting superior technology, DBC as exemplified by Eiffel, has another decade to go. But if Java succeeds in becoming a widely-used language and JavaBeans become a widespread form of reuse then it would already be too late for DBC to have an impact. iContract will be a hardly-noticed event much like ANNA for Ada and A++ for C++. This is because the philosophy/mindset/culture is established by the initial publication of the language and its standard library.
Secondly, these "obvious" benefits. If they're obvious, I want to know why
aren't you using Eiffel? It's a programming language designed around DbC
concepts. It's been around for three decades, at least as long as Python or
longer. There's an existing base of compilers and support tools and libraries
and textbooks and experienced programmers to work with.
Could it be that Python has better libraries, is faster to develop for, attracts
more programmers? If so, I suggest it's worth considering that this might
be *because* Python doesn't have DbC.
And I wouldn't use DbC for Python because
I wouldn't find it helpful for the kind of dynamic, exploratory development
I do in Python. I don't write strict contracts for Python code because in a
dynamically typed, and duck typed, programming language they just don't
make sense to me. Which is not to say I think Design by Contract is bad,
just that it isn't good for Python.
Regards,
Angus
On 23 Sep 2018, at 11:33, Hugh Fisher <hugo....@gmail.com> wrote:Could it be that Python has better libraries, is faster to develop for, attracts
more programmers? If so, I suggest it's worth considering that this might
be *because* Python doesn't have DbC.
On 24 Sep 2018, at 20:09, Marko Ristin-Kaufmann <marko....@gmail.com> wrote:Hi Barry,I think the main issue with pyffel is that it can not support function calls in general. If I understood it right, and Angus please correct me, you would need to wrap every function that you would call from within the contract.
But the syntax is much nicer than icontract or dpcontracts (see these packages on pypi). What if we renamed "args" argument and "old" argument in those libraries to just "a" and "o", respectively? Maybe that gives readable code without too much noise:
@requires(lambda self, a, o: self.sum == o.sum - a.amount)def withdraw(amount: int) -> None:...There is this lambda keyword in front, but it's not too bad?
Note that this means you cannot use macros in a file that is run directly, as it will not be passed through the import hooks.
packagery.resolve_initial_paths(initial_paths)Resolve the initial paths of the dependency graph by recursively adding *.py files beneath given directories.
| Parameters: | initial_paths ( |
|---|---|
| Return type: |
|
| Returns: | list of initial files (i.e. no directories) |
| Requires: |
|
| Ensures: |
|
|---|
|
You'll lose folks attention very quickly when you try to tell folk
what they do and don't understand.
Claiming that DbC annotations will improve the documentation of every
single library on PyPI is an extraordinary claim, and such claims
require extraordinary proof.
I can think of many libraries where necessary pre and post conditions
(such as 'self is still locked') are going to be noisy, and at risk of
reducing comprehension if the DbC checks are used to enhance/extended
documentation.
Some of the examples you've been giving would be better expressed with
a more capable type system in my view (e.g. Rust's), but I have no
good idea about adding that into Python :/.
> As soon as you need to document your code, and
> this is what most modules have to do in teams of more than one person
> (especially so if you are developing a library for a wider audience), you
> need to write down the contracts. Please see above where I tried to
> explained that 2-5) are inferior approaches to documenting contracts
> compared to 1).
You left off option 6), plain text. Comments. Docstrings.
2) Write precondtions and postconditions in docstring of the method as human text.
In Python we can write something like:
def foo(x):
x.bar(y)
What's the type of x? What's the type of y? What is the contract of bar?
Don't know, don't care. x, or y, can be an instance, a class, a module, a
proxy for a remote web service. The only "contract" is that object x will
respond to message bar that takes one argument. Object x, do whatever
you want with it.
ndarray.transpose(*axes)Returns a view of the array with axes transposed.
For a 1-D array, this has no effect. (To change between column and
row vectors, first cast the 1-D array into a matrix object.)
For a 2-D array, this is the usual matrix transpose.
For an n-D array, if axes are given, their order indicates how the
axes are permuted (see Examples). If axes are not provided and
a.shape = (i[0], i[1], ... i[n-2], i[n-1]), then
a.transpose().shape = (i[n-1], i[n-2], ... i[1], i[0]).
As for 4) reading the code, why not? "Use the source, Luke" is now a
programming cliche because it works. It's particularly appropriate for
Python packages which are usually distributed in source form and, as
you yourself noted, easy to read.
I use DbC occasionally to clarify my thoughts during a
refactoring, and then only in the places that continue to make
mistakes. In general, I am not in a domain that benefits from DbC.
Contracts are code: More code means more bugs. Declarative
contracts are succinct, but difficult to debug when wrong; I
believe this because the debugger support for contracts is poor;
There is no way to step through the logic and see the intermediate
reasoning in complex contracts. A contract is an incomplete
duplication of what the code already does: at some level of
complexity I prefer to use a duplicate independent implementation
and compare inputs/outputs.
When you are documenting a method you have the following options:
1) Write preconditions and postconditions formally and include them automatically in the documentation (e.g., by using icontract library).
2) Write precondtions and postconditions in docstring of the method as human text.
3) Write doctests in the docstring of the method.
4) Expect the user to read the actual implementation.
5) Expect the user to read the testing code.
This is again something that eludes me and I would be really thankful if you could clarify. Please consider for an example, pypackagery (https://pypackagery.readthedocs.io/en/latest/packagery.html) and the documentation of its function resolve_initial_paths:
packagery.resolve_initial_paths(initial_paths)Resolve the initial paths of the dependency graph by recursively adding
*.pyfiles beneath given directories.
Parameters: initial_paths (
List[Path]) – initial paths as absolute pathsReturn type:
List[Path]Returns: list of initial files (i.e. no directories)
Requires:
all(pth.is_absolute() for pth in initial_paths)Ensures:
len(result) >= len(initial_paths) if initial_paths else result == []all(pth.is_absolute() for pth in result)all(pth.is_file() for pth in result)
How is this difficult to read,[...]?
Does it work on Windows?resolve_initial_path() is a piece code is better understood by looking at the callers (#7), or not exposing it publicly (#11). You can also use a different set of abstractions, to make the code easier to read:
What is_absolute()? is "file:///" absolute?
How does this code fail?
What does a permission access problem look like?
Can initial_paths can be None?
Can initial_paths be files? directories?
What are the side effects?
An extraordinary claim is like "DbC can improve *every single project*
on PyPI". That requires a TON of proof. Obviously we won't quibble if
you can only demonstrate that 99.95% of them can be improved, but you
have to at least show that the bulk of them can.
6) The name of the method
7) How the method is called throughout the codebase
10) relying on convention inside, and outside, the application
8) observing input and output values during debugging
9) observing input and output values in production
11) Don't communicate - Sometimes <complexity>/<num_customers> is too high; code is not repaired, only replaced.
Does it work on Windows?
What is_absolute()? is "file:///" absolute?
At a high level, I can see the allure of DbC: Programming can be a craft, and a person can derive deep personal satisfaction from perfecting the code they work on. DbC provides you with more decoration, more elaboration, more ornamentation, more control. This is not bad, but I see all your arguments as personal ascetic sense. DbC is only appealing under certain accounting rules. Please consider the possibility that "the best code" is: low $$$, buggy, full of tangles, and mostly gets the job done. :)
It's easy to show beautiful examples that may actually depend on other
things. Whether that's representative of all contracts is another
question.
> * There are always contracts, they can be either implicit or explicit. You need always to figure them out before you call a function or use its result.
Not all code has such contracts. You could argue that code which does
not is inferior to code which does, but not everything follows a
strictly-definable pattern.
> * The are tools for formal contracts.
That's the exact point you're trying to make, so it isn't evidence for
itself. Tools for formal contracts exist as third party in Python, and
if that were good enough for you, we wouldn't be discussing this.
There are no such tools in the standard library or language that make
formal contracts easy.
Disagreed. I would most certainly NOT assume that every reader knows
any particular syntax for such contracts. However, this is a weaker
point.
You might argue that a large proportion of PyPI projects will be
"library-style" packages, where the main purpose is to export a bunch
of functions. But even then, I'm not certain that they'd all benefit
from DbC.
People have said the same thing about type checking, too. Would
*every* project on PyPI benefit from MyPy's type checks? No. Syntax
for them was added, not because EVERYONE should use them, but because
SOME will use them, and it's worth having some language support. You
would probably do better to argue along those lines than to try to
claim that every single project ought to be using contracts.
but I'm still -0.5 on adding anything of the sort to the stdlib, as I
don't yet see that *enough* projects would actually benefit.
It's easy to say that they're boolean expressions. But that's like
saying that unit tests are just a bunch of boolean expressions too.
Why do we have lots of different forms of test, rather than just a big
fat "assert this and this and this and this and this and this"?
Because the key to unit testing is not "boolean expressions", it's a
language that can usefully describe what it is we're testing.
Contracts aren't just boolean expressions - they're a language (or a
mini-language) that lets you define WHAT the contract entails.
> Thanks for this clarification (and the download-yt example)! I actually only had packages-as-libraries in mind, not the executable scripts; my mistake. So, yes, "any Pypi package" should be reformulated to "any library on Pypi" (meant to be used by a wider audience than the developers themselves).
>
Okay. Even with that qualification, though, I still think that not
every library will benefit from this. For example, matplotlib's
plt.show() method guarantees that... a plot will be shown, and the
user will have dismissed it, before it returns. Unless you're inside
Jupyter/iPython, in which case it's different. Or if you're in certain
other environments, in which case it's different again. How do you
define the contract for something that is fundamentally interactive?
You can define very weak contracts. For instance, input() guarantees
that its return value is a string. Great! DbC doing the job of type
annotations. Can you guarantee anything else about that string? Is
there anything else useful that can be spelled easily?
matplotlib.pyplot.show(*args, **kw)[source]Display a figure. When running in ipython with its pylab mode, display all figures and return to the ipython prompt.
In non-interactive mode, display all figures and block until the figures have been closed; in interactive mode it has no effect unless figures were created prior to a change from non-interactive to interactive mode (not recommended). In that case it displays the figures but does not block.
A single experimental keyword argument, block, may be set to True or False to override the blocking behavior described above.
In non-interactive mode, display all figures and block until the figures have been closed; in interactive mode it has no effect unless figures were created prior to a change from non-interactive to interactive mode (not recommended). In that case it displays the figures but does not block.
A single experimental keyword argument, block, may be set to True or False to override the blocking behavior described above.
For most single use (or infrequently used) functions, I'd argue that the trade-off *isn't* worth it.
Here's a quick example from the pip codebase:
# Retry every half second for up to 3 seconds
@retry(stop_max_delay=3000, wait_fixed=500)
def rmtree(dir, ignore_errors=False):
shutil.rmtree(dir, ignore_errors=ignore_errors,
onerror=rmtree_errorhandler)
Also, the implicit contracts code currently has are typically pretty loose. What you need to "figure out" is very general. Explicit contracts are typically demonstrated as being relatively strict, and figuring out and writing such contracts is more work than writing code with loose implicit contracts. Whether the trade-off of defining tight explicit contracts once vs inferring a loose implicit contract every time you call the function is worth it, depends on how often the function is called. For most single use (or infrequently used) functions, I'd argue that the trade-off *isn't* worth it.
def fibber(n):
return n if n < 2 else fibber(n-1) + fibber(n-2)
> Here is my try at the contracts. Assuming that there is a list of figures and that they have a property "displayed" and that "State.blocked" global variable refers to whether the interface is blocked or not::
> @post(lambda: all(figure.displayed for figure in figures)
> @post(lambda: not ipython.in_pylab_mode() or not State.blocked)
> @post(lambda: not interactive() or State.blocked)
> matplotlib.pyplot.show()
>
There is no such thing as "State.blocked". It blocks. The function
*does not return* until the figure has been displayed, and dismissed.
There's no way to recognize that inside the function's state.
Contracts are great when every function is 100% deterministic and can
maintain invariants and/or transform from one set of invariants to
another. Contracts are far less clear when the definitions are
muddier.
> However, contracts can be useful when testing the GUI -- often it is difficult to script the user behavior. What many people do is record a session and re-play it. If there is a bug, fix it. Then re-record. While writing unit tests for GUI is hard since GUI changes rapidly during development and scripting formally the user behavior is tedious, DbC might be an alternative where you specify as much as you can, and then just re-run through the session. This implies, of course, a human tester.
>
That doesn't sound like the function's contract. That sounds like a
test case - of which you would have multiple, using different
"scripted session" inputs and different outputs (some of success, some
of expected failure).
There's certainly benefits for the "contracts as additional tests"
viewpoint. But whenever that's proposed as what people understand by
DbC, the response is "no, it's not like that at all". So going back to
the "why isn't DbC more popular" question - because no-one can get a
clear handle on whether they are "like tests" or "like assertions" or
"something else" :-)
> Please mind that I said: any library would benefit from it, as in the users of any library on pipy would benefit from better, formal and more precise documentation. That doesn't imply that all the contracts need to be specified or that you have to specify the contract for every function, or that you omit the documentation altogether. Some contracts are simply too hard to get explicitly. Some are not meaningful even if you could write them down. Some you'd like to add, but run only at test time since too slow in production. Some run in production, but are not included in the documentation (e.g., to prevent the system to enter a bad state though it is not meaningful for the user to actually read that contract).
>
> Since contracts are formally written, they can be verified. Human text can not. Specifying all the contracts is in most cases not meaningful. In my day-to-day programming, I specify contracts on the fly and they help me express formally to the next girl/guy who will use my code what to expect or what not. That does not mean that s/he can just skip the documentation or that contracts describe fully what the function does. They merely help -- help him/her what arguments and results are expected. That does not mean that I fully specified all the predicates on the arguments and the result. It's merely a help à la
> * "Ah, this argument needs to be bigger than that argument!"
> * "Ah, the resulting dictionary is shorter than the input list!"
> * "Ah, the result does not include the input list"
> * "Ah, this function accepts only files (no directories) and relative paths!"
> * "Ah, I put the bounding box outside of the image -- something went wrong!"
> * "Ah, this method allows me to put the bounding box outside of the image and will fill all the outside pixels with black!" etc.
Whoops, I think the rules changed under me again :-(
Are we talking here about coding explicit executable contracts in the
source code of the library, or using (formally described in terms of
(pseudo-)code) contract-style descriptions in the documentation, or
simply using the ideas of contract-based thinking in designing and
writing code?
> For example, if I have an object detector operating on a region-of-interest and returning bounding boxes of the objects, the postconditions will not be: "all the bounding boxes are cars", that would impossible. But the postcondition might check that all the bounding boxes are within the region-of-interest or slightly outside, but not completely outside etc.
I understand that you have to pick an appropriate level of strictness
when writing contracts. That's not ever been in question (at least in
my mind).
> Let's be careful not to make a straw-man here, i.e. to push DbC ad absurdum and then discard it that way.
I'm not trying to push DbC to that point. What I *am* trying to do is
make it clear that your arguments (and in particular the fact that you
keep insisting that "everything" can benefit) are absurd. If you'd
tone back on the extreme claims (as Chris has also asked) then you'd
be more likely to get people interested. This is why (as you
originally asked) DbC is not more popular - its proponents don't seem
to be able to accept that it might not be the solution to every
problem. Python users are typically fairly practical, and think in
terms of "if it helps in this situation, I'll use it". Expecting them
to embrace an argument that demands they accept it applies to
*everything* is likely to meet with resistance.
You didn't address my question "does this apply to the stdlib"? If it
doesn't, your argument has a huge hole - how did you decide that the
solution you're describing as "beneficial to all libraries" doesn't
improve the stdlib? If it does, then why not demonstrate your case?
Give concrete examples - look at some module in the stdlib (for
example, pathlib) and show exactly what contracts you'd add to the
code, what the result would look like to the library user (who
normally doesn't read the source code) and to the core dev (who does).
Remember that pathlib (like all of the stdlib) doesn't use type
annotations, and that is a *deliberate* choice, mandated by Guido when
he first introduced type annotations. So you're not allowed to add
contracts like "arg1 is a string", nor are you allowed to say that the
lack of type annotations makes the exercise useless.
I think I've probably said all I can usefully say here. If you do
write up a DbC-enhanced pathlib, I'll be interested in seeing it and
may well have more to say as a result. If not, I think I'm just going
to file your arguments as "not proven".
Are we talking here about coding explicit executable contracts in the
source code of the library, or using (formally described in terms of
(pseudo-)code) contract-style descriptions in the documentation, or
simply using the ideas of contract-based thinking in designing and
writing code?
You didn't address my question "does this apply to the stdlib"? If it
doesn't, your argument has a huge hole - how did you decide that the
solution you're describing as "beneficial to all libraries" doesn't
improve the stdlib? If it does, then why not demonstrate your case?
Give concrete examples - look at some module in the stdlib (for
example, pathlib) and show exactly what contracts you'd add to the
code, what the result would look like to the library user (who
normally doesn't read the source code) and to the core dev (who does).
Remember that pathlib (like all of the stdlib) doesn't use type
annotations, and that is a *deliberate* choice, mandated by Guido when
he first introduced type annotations. So you're not allowed to add
contracts like "arg1 is a string", nor are you allowed to say that the
lack of type annotations makes the exercise useless.
Sent from my iPhone
> On Sep 26, 2018, at 3:18 AM, Chris Angelico <ros...@gmail.com> wrote:
>
> It's easy to say that they're boolean expressions. But that's like
> saying that unit tests are just a bunch of boolean expressions too.
> Why do we have lots of different forms of test, rather than just a big
> fat "assert this and this and this and this and this and this"?
> Because the key to unit testing is not "boolean expressions", it's a
> language that can usefully describe what it is we're testing.
> Contracts aren't just boolean expressions - they're a language (or a
> mini-language) that lets you define WHAT the contract entails.
It's the reason why type checking exists, and why bounds checking exists, and why unit checking exists too.
On Thu, Sep 27, 2018 at 8:53 AM Robert Collins
<rob...@robertcollins.net> wrote:
>
> On Thu, 27 Sep 2018 at 00:44, Marko Ristin-Kaufmann
> <marko....@gmail.com> wrote:
> >
> > P.S. My offer still stands: I would be very glad to annotate with contracts a set of functions you deem representative (e.g., from a standard library or from some widely used library). Then we can discuss how these contracts. It would be an inaccurate estimate of the benefits of DbC in Python, but it's at least better than no estimate. We can have as little as 10 functions for the start. Hopefully a couple of other people would join, so then we can even see what the variance of contracts would look like.
>
> i think requests would be a very interesting library to annotate. Just
> had a confused developer wondering why calling an API with
> session.post(...., data={...some object dict here}) didn't work
> properly. (Solved by s/data/json), but perhaps illustrative of
> something this might help with?
Not sure what you mean by not working; my suspicion is that it DID
work, but didn't do what you thought it did (it would form-encode).
Contracts wouldn't help there, because it's fully legal and correct.
Obvious benefitsYou both seem to misconceive the contracts. The goal of the design-by-contract is not reduced to testing the correctness of the code, as I reiterated already a couple of times in the previous thread. The contracts document formally what the caller and the callee expect and need to satisfy when using a method, a function or a class. This is meant for a module that is used by multiple people which are not necessarily familiar with the code. They are not a niche. There are 150K projects on pypi.org. Each one of them would benefit if annotated with the contracts.
Contracts are difficult to read.David wrote:To me, as I've said, DbC imposes a very large cost for both writers and readers of code.This is again something that eludes me and I would be really thankful if you could clarify. Please consider for an example, pypackagery (https://pypackagery.readthedocs.io/en/latest/packagery.html) and the documentation of its function resolve_initial_paths:
packagery.resolve_initial_paths(initial_paths)Resolve the initial paths of the dependency graph by recursively adding
*.pyfiles beneath given directories.
Parameters: initial_paths (
List[Path]) – initial paths as absolute pathsReturn type:
List[Path]Returns: list of initial files (i.e. no directories)
Requires:
all(pth.is_absolute() for pth in initial_paths)Ensures:
len(result) >= len(initial_paths) if initial_paths else result == []all(pth.is_absolute() for pth in result)all(pth.is_file() for pth in result)
Writing contracts is difficult.David wrote:To me, as I've said, DbC imposes a very large cost for both writers and readers of code.The effort of writing contracts include as of now:* include icontract (or any other design-by-contract library) to setup.py (or requirements.txt), one line one-off* include sphinx-icontract to docs/source/conf.py and docs/source/requirements.txt, two lines, one-off* write your contracts (usually one line per contract).
I think that ignorance plays a major role here. Many people have misconceptions about the design-by-contract. They just use 2) for more complex methods, or 3) for rather trivial methods. They are not aware that it's easy to use the contracts (1) and fear using them for non-rational reasons (e.g., habits).
rmdir()Remove this directory. The directory must be empty.
| Requires: |
|
|---|
stat()Return the result of the stat() system call on this path, like os.stat() does.
| Ensures: |
|
|---|
Hi,I annotated pathlib with contracts:https://github.com/mristin/icontract-pathlib-poc. I zipped the HTML docs into https://github.com/mristin/icontract-pathlib-poc/blob/master/contracts-pathlib-poc.zip, you can just unpack and view the index.html.
Some of the contracts might seem trivial -- but mind that you, as a writer, want to tell the user what s/he is expected to fulfill before calling the function. For example, if you say:
rmdir()Remove this directory. The directory must be empty.
Requires:
not list(self.iterdir())(??? There must be a way to check this more optimally)self.is_dir()self.is_dir() contract might seem trivial -- but it's actually not. You actually want to convey the message: dear user, if you are iterating through a list of paths, use this function to decide if you should call rmdir() or unlink(). Analogously with the first contract: dear user, please check if the directory is empty before calling rmdir() and this is what you need to call to actually check that.
I also finally assembled the puzzle. Most of you folks are all older and were exposed to DbC in the 80ies championed by DbC zealots who advertised it as the tool for software development. You were repulsed by their fanaticism -- the zealots also pushed for all the contracts to be defined, and none less. Either you have 100% DbC or no sane software development at all.
And that's why I said that the libraries on pypi meant to be used by multiple people and which already have type annotations would obviously benefit from contracts -- while you were imagining that all of these libraries need to be DbC'ed 100%, I was imagining something much more humble. Thus the misunderstanding.
After annotating pathlib, I find that it actually needs contracts more thain if it had type annotations. For example:
stat()Return the result of the stat() system call on this path, like os.stat() does.
Ensures:
result is not None⇒self.exists()result is not None⇒os.stat(str(self)).__dict__ == result.__dict__(??? This is probably not what it was meant with ‘like os.stat() does’?)But what does it mean "like os.stat() does"? I wrote equality over __dict__'s in the contract. That was my idea of what the implementer was trying to tell me. But is that the operator that should be applied? Sure, the contract merits a description. But without it, how am I supposed to know what "like" means?Similarly with rmdir() -- "the directory must be empty" -- but how exactly am I supposed to check that?
Anyhow, please have a look at the contracts and let me know what you think. Please consider it an illustration. Try to question whether the contracts I wrote are so obvious to everybody even if they are obvious to you and keep in mind that the user does not look into the implementation. And please try to abstract away the aesthetics: neither icontract library that I wrote nor the sphinx extension are of sufficient quality. We use them for our company code base, but they still need a lot of polishing. So please try to focus only on the content. We are still talking about contracts in general, not about the concrete contract implementation
On Fri, 28 Sep 2018 at 02:24, Marko Ristin-Kaufmann <marko....@gmail.com> wrote:I annotated pathlib with contracts:https://github.com/mristin/icontract-pathlib-poc. I zipped the HTML docs into https://github.com/mristin/icontract-pathlib-poc/blob/master/contracts-pathlib-poc.zip, you can just unpack and view the index.html.
The thing that you didn't discuss in the above was the effect on the source code. Looking at your modified sources, I found it *significantly* harder to read your annotated version than the original. Basically every function and class was cluttered with irrelevant[1] conditions, which obscured the logic and the basic meaning of the code.
Try to question whether the contracts I wrote are so obvious to everybody even if they are obvious to you and keep in mind that the user does not look into the implementation.
I'm ambivalent about the Sphinx examples. I find the highly detailed
code needed to express a condition fairly unreadable (and I'm an
experienced Python programmer). For example
@pre(lambda args, result: not any(Path(arg).is_absolute() for arg in args) or
(result == [pth for arg in args for pth in [Path(arg)] if
pth.is_absolute()][-1]),
"When several absolute paths are given, the last is taken as an anchor
(mimicking os.path.join()’s behaviour)")
The only way I'd read that is by looking at the summary text - I'd
never get the sense of what was going on from the code alone. There's
clearly a number of trade-offs going on here:
* Conditions should be short, to avoid clutter
* Writing helper functions that are *only* used in conditions is more
code to test or get wrong
* Sometimes it's just plain hard to express a verbal constraint in code
* Marko isn't that familiar with the codebase, so there may be better
ways to express certain things
But given that *all* the examples I've seen of contracts have this
issue (difficult to read expressions) I suspect the problem is
inherent.
Another thing that I haven't yet seen clearly explained. How do these
contracts get *run*? Are they checked on every call to the function,
even in production code? Is there a means to turn them off? What's the
runtime overhead of a "turned off" contract (even if it's just an
if-test per condition, that can still add up)? And what happens if a
contract fails - is it an exception/traceback (which is often
unacceptable in production code such as services)? The lack of any
clear feel for the cost of adding contracts is part of what makes
people reluctant to use them (particularly in the face of the
unfortunately still common assertion that "Python is slow" :-()
Similarly with rmdir() -- "the directory must be empty" -- but how exactly am I supposed to check that?
Isn't that the whole point? The prose statement "the directory must be empty" is clear. But the exact code check isn't - and may be best handled by a series of unit tests, rather than a precondition.
* Marko isn't that familiar with the codebase, so there may be better
ways to express certain things
* Sometimes it's just plain hard to express a verbal constraint in code
@pre(lambda args, result: not any(Path(arg).is_absolute() for arg in args) or
(result == [pth for arg in args for pth in [Path(arg)] if
pth.is_absolute()][-1]),
"When several absolute paths are given, the last is taken as an anchor
(mimicking os.path.join()’s behaviour)")
It is still fundamentally difficult to make assertions about the file
system as pre/post contracts. Are you becoming aware of this?
Contracts, as has been stated multiple times, look great for
mathematically pure functions that have no state outside of their own
parameters and return values (and 'self', where applicable), but are
just poor versions of unit tests when there's anything external to
consider.
On Wed, Sep 26, 2018 at 04:03:16PM +0100, Rhodri James wrote:
> Assuming that you
> aren't doing some kind of wide-ranging static analysis (which doesn't
> seem to be what we're talking about), all that the contracts have bought
> you is the assurance that *this* invocation of the function with *these*
> parameters giving *this* result is what you expected. It does not say
> anything about the reliability of the function in general.
This is virtually the complete opposite of what contracts give us. What
you are describing is the problem with *unit testing*, not contracts.
- Half of the checks are very far away, in a separate file, assuming
I even remembered or bothered to write the test.
- The post-conditions aren't checked unless I run my test suite, and
then they only check the canned input in the test suite.
- No class invariants.
- Inheritance is not handled correctly.
On Sun, Sep 30, 2018 at 10:29:50AM -0400, David Mertz wrote:
But given that in general unit tests tend to only exercise a handful of
values (have a look at the tests in the Python stdlib) I think it is
fair to say that in practice unit tests typically do not have anywhere
near the coverage of live data used during alpha and beta testing.
I'm curious. When you write a function or method, do you include input
checks? Here's an example from the Python stdlib (docstring removed for
brevity):
# bisect.py
def insort_right(a, x, lo=0, hi=None):
if lo < 0:
raise ValueError('lo must be non-negative')
if hi is None:
hi = len(a)
while lo < hi:
mid = (lo+hi)//2
if x < a[mid]: hi = mid
else: lo = mid+1
a.insert(lo, x)
Do you consider that check for lo < 0 to be disruptive? How would you
put that in a unit test?