mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge branch 'main' into review-styles-reference
This commit is contained in:
@@ -12,5 +12,4 @@ repos:
|
|||||||
rev: 22.10.0
|
rev: 22.10.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
exclude: ^tests/
|
|
||||||
exclude: ^tests/snapshot_tests
|
exclude: ^tests/snapshot_tests
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
- `MouseScrollUp` and `MouseScrollDown` now inherit from `MouseEvent` and have attached modifier keys. https://github.com/Textualize/textual/pull/1458
|
- `MouseScrollUp` and `MouseScrollDown` now inherit from `MouseEvent` and have attached modifier keys. https://github.com/Textualize/textual/pull/1458
|
||||||
- Fail-fast and print pretty tracebacks for Widget compose errors https://github.com/Textualize/textual/pull/1505
|
- Fail-fast and print pretty tracebacks for Widget compose errors https://github.com/Textualize/textual/pull/1505
|
||||||
|
- Added Widget._refresh_scroll to avoid expensive layout when scrolling https://github.com/Textualize/textual/pull/1524
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
323
docs/blog/posts/looking-for-help.md
Normal file
323
docs/blog/posts/looking-for-help.md
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
---
|
||||||
|
draft: false
|
||||||
|
date: 2023-01-09
|
||||||
|
categories:
|
||||||
|
- DevLog
|
||||||
|
authors:
|
||||||
|
- davep
|
||||||
|
---
|
||||||
|
|
||||||
|
# So you're looking for a wee bit of Textual help...
|
||||||
|
|
||||||
|
## Introduction
|
||||||
|
|
||||||
|
!!! quote
|
||||||
|
|
||||||
|
Patience, Highlander. You have done well. But it'll take time. You are
|
||||||
|
generations being born and dying. You are at one with all living things.
|
||||||
|
Each man's thoughts and dreams are yours to know. You have power beyond
|
||||||
|
imagination. Use it well, my friend. Don't lose your head.
|
||||||
|
|
||||||
|
<cite>Juan Sánchez Villalobos Ramírez, Chief metallurgist to King Charles V of Spain</cite>
|
||||||
|
|
||||||
|
As of the time of writing, I'm a couple or so days off having been with
|
||||||
|
Textualize for 3 months. It's been fun, and educational, and every bit as
|
||||||
|
engaging as I'd hoped, and more. One thing I hadn't quite prepared for
|
||||||
|
though, but which I really love, is how so many other people are learning
|
||||||
|
Textual along with me.
|
||||||
|
|
||||||
|
<!-- more -->
|
||||||
|
|
||||||
|
Even in those three months the library has changed and expanded quite a lot,
|
||||||
|
and it continues to do so. Meanwhile, more people are turning up and using
|
||||||
|
the framework; you can see this online in social media, blogs and of course
|
||||||
|
[in the ever-growing list of projects on GitHub which depend on
|
||||||
|
Textual](https://github.com/Textualize/textual/network/dependents).
|
||||||
|
|
||||||
|
This inevitably means there's a lot of people getting to grips with a new
|
||||||
|
tool, and one that is still a bit of a moving target. This in turn means
|
||||||
|
lots of people are coming to us to get help.
|
||||||
|
|
||||||
|
As I've watched this happen I've noticed a few patterns emerging. Some of
|
||||||
|
these good or neutral, some... let's just say not really beneficial to those
|
||||||
|
seeking the help, or to those trying to provide the help. So I wanted to
|
||||||
|
write a little bit about the different ways you can get help with Textual
|
||||||
|
and your Textual-based projects, and to also try and encourage people to
|
||||||
|
take the most helpful and positive approach to getting that help.
|
||||||
|
|
||||||
|
Now, before I go on, I want to make something *very* clear: I'm writing this
|
||||||
|
as an individual. This is my own personal view, and my own advice from me to
|
||||||
|
anyone who wishes to take it. It's not Textual (the project) or Textualize
|
||||||
|
(the company) policy, rules or guidelines. This is just some ageing hacker's
|
||||||
|
take on how best to go about asking for help, informed by years of asking
|
||||||
|
for and also providing help in email, on Usenet, on forums, etc.
|
||||||
|
|
||||||
|
Or, put another way: if what you read in here seems sensible to you, I
|
||||||
|
figure we'll likely have already hit it off [over on
|
||||||
|
GitHub](https://github.com/Textualize/textual) or in [the Discord
|
||||||
|
server](https://discord.gg/Enf6Z3qhVr). ;-)
|
||||||
|
|
||||||
|
## Where to go for help
|
||||||
|
|
||||||
|
At this point this is almost a bit of an FAQ itself, so I thought I'd
|
||||||
|
address it here: where's the best place to ask for help about Textual, and
|
||||||
|
what's the difference between GitHub Issues, Discussions and our Discord
|
||||||
|
server?
|
||||||
|
|
||||||
|
I'd suggest thinking of them like this:
|
||||||
|
|
||||||
|
### Discord
|
||||||
|
|
||||||
|
You have a question, or need help with something, and perhaps you could do
|
||||||
|
with a reply as soon as possible. But, and this is the **really important
|
||||||
|
part**, it doesn't matter if you don't get a response. If you're in this
|
||||||
|
situation then the Discord server is possibly a good place to start. If
|
||||||
|
you're lucky someone will be hanging about who can help out.
|
||||||
|
|
||||||
|
I can't speak for anyone else, but keep this in mind: when I look in on
|
||||||
|
Discord I tend not to go scrolling back much to see if anything has been
|
||||||
|
missed. If something catches my eye, I'll try and reply, but if it
|
||||||
|
doesn't... well, it's mostly an instant chat thing so I don't dive too
|
||||||
|
deeply back in time.
|
||||||
|
|
||||||
|
!!! tip inline end "Going from Discord to a GitHub issue"
|
||||||
|
|
||||||
|
As a slight aside here: sometimes people will pop up in Discord, ask a
|
||||||
|
question about something that turns out looking like a bug, and that's
|
||||||
|
the last we hear of it. Please, please, **please**, if this happens, the
|
||||||
|
most helpful thing you can do is go raise an issue for us. It'll help us
|
||||||
|
to keep track of problems, it'll help get your problem fixed, it'll mean
|
||||||
|
everyone benefits.
|
||||||
|
|
||||||
|
My own advice would be to treat Discord as an ephemeral resource. It happens
|
||||||
|
in the moment but fades away pretty quickly. It's like knocking on a
|
||||||
|
friend's door to see if they're in. If they're not in, you might leave them
|
||||||
|
a note, which is sort of like going to...
|
||||||
|
|
||||||
|
### GitHub
|
||||||
|
|
||||||
|
On the other hand, if you have a question or need some help or something
|
||||||
|
where you want to stand a good chance of the Textual developers (amongst
|
||||||
|
others) seeing it and responding, I'd recommend that GitHub is the place to
|
||||||
|
go. Dropping something into the discussions there, or leaving an issue,
|
||||||
|
ensures it'll get seen. It won't get lost.
|
||||||
|
|
||||||
|
As for which you should use -- a discussion or an issue -- I'd suggest this:
|
||||||
|
if you need help with something, or you want to check your understanding of
|
||||||
|
something, or you just want to be sure something is a problem before taking
|
||||||
|
it further, a discussion might be the best thing. On the other hand, if
|
||||||
|
you've got a clear bug or feature request on your hands, an issue makes a
|
||||||
|
lot of sense.
|
||||||
|
|
||||||
|
Don't worry if you're not sure which camp your question or whatever falls
|
||||||
|
into though; go with what you think is right. There's no harm done either
|
||||||
|
way (I may move an issue to a discussion first before replying, if it's
|
||||||
|
really just a request for help -- but that's mostly so everyone can benefit
|
||||||
|
from finding it in the right place later on down the line).
|
||||||
|
|
||||||
|
## The dos and don'ts of getting help
|
||||||
|
|
||||||
|
Now on to the fun part. This is where I get a bit preachy. Ish. Kinda. A
|
||||||
|
little bit. Again, please remember, this isn't a set of rules, this isn't a
|
||||||
|
set of official guidelines, this is just a bunch of *"if you want my advice,
|
||||||
|
and I know you didn't ask but you've read this far so you actually sort of
|
||||||
|
did don't say I didn't warn you!"* waffle.
|
||||||
|
|
||||||
|
This isn't going to be an exhaustive collection, far from it. But I feel
|
||||||
|
these are some important highlights.
|
||||||
|
|
||||||
|
### Do...
|
||||||
|
|
||||||
|
When looking for help, in any of the locations mentioned above, I'd totally
|
||||||
|
encourage:
|
||||||
|
|
||||||
|
#### Be clear and detailed
|
||||||
|
|
||||||
|
Too much detail is almost always way better than not enough. *"My program
|
||||||
|
didn't run"*, often even with some of the code supplied, is so much harder
|
||||||
|
to help than *"I ran this code I'm posting here, and I expected this
|
||||||
|
particular outcome, and I expected it because I'd read this particular thing
|
||||||
|
in the docs and had comprehended it to mean this, but instead the outcome
|
||||||
|
was this exception here, and I'm a bit stuck -- can someone offer some
|
||||||
|
pointers?"*
|
||||||
|
|
||||||
|
The former approach means there often ends up having to be a back and forth
|
||||||
|
which can last a long time, and which can sometimes be frustrating for the
|
||||||
|
person asking. Manage frustration: be clear, tell us everything you can.
|
||||||
|
|
||||||
|
#### Say what resources you've used already
|
||||||
|
|
||||||
|
If you've read the potions of the documentation that relate to what you're
|
||||||
|
trying to do, it's going to be really helpful if you say so. If you don't,
|
||||||
|
it might be assumed you haven't and you may end up being pointed at them.
|
||||||
|
|
||||||
|
So, please, if you've checked the documentation, looked in the FAQ, done a
|
||||||
|
search of past issues or discussions or perhaps even done a search on the
|
||||||
|
Discord server... please say so.
|
||||||
|
|
||||||
|
#### Be polite
|
||||||
|
|
||||||
|
This one can go a long way when looking for help. Look, I get it,
|
||||||
|
programming is bloody frustrating at times. We've all rage-quit some code at
|
||||||
|
some point, I'm sure. It's likely going to be your moment of greatest
|
||||||
|
frustration when you go looking for help. But if you turn up looking for
|
||||||
|
help acting all grumpy and stuff it's not going to come over well. Folk are
|
||||||
|
less likely to be motivated to lend a hand to someone who seems rather
|
||||||
|
annoyed.
|
||||||
|
|
||||||
|
If you throw in a please and thank-you here and there that makes it all the
|
||||||
|
better.
|
||||||
|
|
||||||
|
#### Fully consider the replies
|
||||||
|
|
||||||
|
You could find yourself getting a reply that you're sure won't help at all.
|
||||||
|
That's fair. But be sure to fully consider it first. Perhaps you missed the
|
||||||
|
obvious along the way and this is 100% the course correction you'd
|
||||||
|
unknowingly come looking for in the first place. Sure, the person replying
|
||||||
|
might have totally misunderstood what was being asked, or might be giving a
|
||||||
|
wrong answer (it me! I've totally done that and will again!), but even then
|
||||||
|
a reply along the lines of *"I'm not sure that's what I'm looking for,
|
||||||
|
because..."* gets everyone to the solution faster than *"lol nah"*.
|
||||||
|
|
||||||
|
#### Entertain what might seem like odd questions
|
||||||
|
|
||||||
|
Aye, I get it, being asked questions when you're looking for an *answer* can
|
||||||
|
be a bit frustrating. But if you find yourself on the receiving end of a
|
||||||
|
small series of questions about your question, keep this in mind: Textual is
|
||||||
|
still rather new and still developing and it's possible that what you're
|
||||||
|
trying to do isn't the correct way to do that thing. To the person looking
|
||||||
|
to help you it may seem to them you have an [XY
|
||||||
|
problem](https://en.wikipedia.org/wiki/XY_problem).
|
||||||
|
|
||||||
|
Entertaining those questions might just get you to the real solution to your
|
||||||
|
problem.
|
||||||
|
|
||||||
|
#### Allow for language differences
|
||||||
|
|
||||||
|
You don't need me to tell you that a project such as Textual has a global
|
||||||
|
audience. With that rather obvious fact comes the other fact that we don't
|
||||||
|
all share the same first language. So, please, as much as possible, try and
|
||||||
|
allow for that. If someone is trying to help you out, and they make it clear
|
||||||
|
they're struggling to follow you, keep this in mind.
|
||||||
|
|
||||||
|
#### Acknowledge the answer
|
||||||
|
|
||||||
|
I suppose this is a variation on "be polite" (really, a thanks can go a long
|
||||||
|
way), but there's more to this than a friendly acknowledgement. If someone
|
||||||
|
has gone to the trouble of offering some help, it's helpful to everyone who
|
||||||
|
comes after you to acknowledge if it worked or not. That way a future
|
||||||
|
help-seeker will know if the answer they're reading stands a chance of being
|
||||||
|
the right one.
|
||||||
|
|
||||||
|
#### Accept that Textual is zero-point software (right now)
|
||||||
|
|
||||||
|
Of course the aim is to have every release of Textual be stable and useful,
|
||||||
|
but things will break. So, please, do keep in mind things like:
|
||||||
|
|
||||||
|
- Textual likely doesn't have your feature of choice just yet.
|
||||||
|
- We might accidentally break something (perhaps pinning Textual and testing
|
||||||
|
each release is a good plan here?).
|
||||||
|
- We might deliberately break something because we've decided to take a
|
||||||
|
particular feature or way of doing things in a better direction.
|
||||||
|
|
||||||
|
Of course it can be a bit frustrating a times, but overall the aim is to
|
||||||
|
have the best framework possible in the long run.
|
||||||
|
|
||||||
|
### Don't...
|
||||||
|
|
||||||
|
Okay, now for a bit of old-hacker finger-wagging. Here's a few things I'd
|
||||||
|
personally discourage:
|
||||||
|
|
||||||
|
#### Lack patience
|
||||||
|
|
||||||
|
Sure, it can be annoying. You're in your flow, you've got a neat idea for a
|
||||||
|
thing you want to build, you're stuck on one particular thing and you really
|
||||||
|
need help right now! Thing is, that's unlikely to happen. Badgering
|
||||||
|
individuals, or a whole resource, to reply right now, or complaining that
|
||||||
|
it's been `$TIME_PERIOD` since you asked and nobody has replied... that's
|
||||||
|
just going to make people less likely to reply.
|
||||||
|
|
||||||
|
#### Unnecessarily tag individuals
|
||||||
|
|
||||||
|
This one often goes hand in hand with the "lack patience" thing: Be it
|
||||||
|
asking on Discord, or in GitHub issues, discussions or even PRs,
|
||||||
|
unnecessarily tagging individuals is a bit rude. Speaking for myself and
|
||||||
|
only myself: I *love* helping folk with Textual. If I could help everyone
|
||||||
|
all the time the moment they have a problem, I would. But it doesn't work
|
||||||
|
like that. There's any number of reasons I might not be responding to a
|
||||||
|
particular request, including but not limited to (here I'm talking
|
||||||
|
personally because I don't want to speak for anyone else, but I'm sure I'm
|
||||||
|
not alone here):
|
||||||
|
|
||||||
|
- I have a job. Sure, my job is (in part) Textual, but there's more to it
|
||||||
|
than that particular issue. I might be doing other stuff.
|
||||||
|
- I have my own projects to work on too. I like coding for fun as well (or
|
||||||
|
writing preaching old dude blog posts like this I guess, but you get the
|
||||||
|
idea).
|
||||||
|
- I actually have other interests outside of work hours so I might actually
|
||||||
|
be out doing a 10k in the local glen, or battling headcrabs in VR, or
|
||||||
|
something.
|
||||||
|
- Housework. :-/
|
||||||
|
|
||||||
|
You get the idea though. So while I'm off having a well-rounded life, it's
|
||||||
|
not good to get unnecessarily intrusive alerts to something that either a)
|
||||||
|
doesn't actually directly involve me or b) could wait.
|
||||||
|
|
||||||
|
#### Seek personal support
|
||||||
|
|
||||||
|
Again, I'm going to speak totally for myself here, but I also feel the
|
||||||
|
general case is polite for all: there's a lot of good support resources
|
||||||
|
available already; sending DMs on Discord or Twitter or in the Fediverse,
|
||||||
|
looking for direct personal support, isn't really the best way to get help.
|
||||||
|
Using the public/collective resources is absolutely the *best* way to get
|
||||||
|
that help. Why's it a bad idea to dive into DMs? Here's some reasons I think
|
||||||
|
it's not a good idea:
|
||||||
|
|
||||||
|
- It's a variation on "unnecessarily tagging individuals".
|
||||||
|
- You're short-changing yourself when it comes to getting help. If you ask
|
||||||
|
somewhere more public you're asking a much bigger audience, who
|
||||||
|
collectively have more time, more knowledge and more experience than a
|
||||||
|
single individual.
|
||||||
|
- Following on from that, any answers can be (politely) fact-checked or
|
||||||
|
enhanced by that audience, resulting in a better chance of getting the
|
||||||
|
best help possible.
|
||||||
|
- The next seeker-of-help gets to miss out on your question and the answer.
|
||||||
|
If asked and answered in public, it's a record that can help someone else
|
||||||
|
in the future.
|
||||||
|
|
||||||
|
#### Doubt your ability or skill level
|
||||||
|
|
||||||
|
I suppose this should really be phrased as a do rather than a don't, as here
|
||||||
|
I want to encourage something positive. A few times I've helped people out
|
||||||
|
who have been very apologetic about their questions being "noob" questions,
|
||||||
|
or about how they're fairly new to Python, or programming in general.
|
||||||
|
Really, please, don't feel the need to apologise and don't be ashamed of
|
||||||
|
where you're at.
|
||||||
|
|
||||||
|
If you've asked something that's obviously answered in the documentation,
|
||||||
|
that's not a problem; you'll likely get pointed at the docs and it's what
|
||||||
|
happens next that's the key bit. If the attitude is *"oh, cool, that's
|
||||||
|
exactly what I needed to be reading, thanks!"* that's a really positive
|
||||||
|
thing. The only time it's a problem is when there's a real reluctance to use
|
||||||
|
the available resources. We've all seen that person somewhere at some point,
|
||||||
|
right? ;-)
|
||||||
|
|
||||||
|
Not knowing things [is totally cool](https://xkcd.com/1053/).
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
So, that's my waffle over. As I said at the start: this is my own personal
|
||||||
|
thoughts on how to get help with Textual, both as someone whose job it is to
|
||||||
|
work on Textual and help people with Textual, and also as a FOSS advocate
|
||||||
|
and supporter who can normally be found helping Textual users when he's not
|
||||||
|
"on the clock" too.
|
||||||
|
|
||||||
|
What I've written here isn't exhaustive. Neither is it novel. Plenty has
|
||||||
|
been written on the general subject in the past, and I'm sure more will be
|
||||||
|
written on the subject in the future. I do, however, feel that these are the
|
||||||
|
most common things I notice. I'd say those dos and don'ts cover 90% of *"can
|
||||||
|
I get some help?"* interactions; perhaps closer to 99%.
|
||||||
|
|
||||||
|
Finally, and I think this is the most important thing to remember, the next
|
||||||
|
time you are battling some issue while working with Textual: [don't lose
|
||||||
|
your head](https://www.youtube.com/watch?v=KdYvKF9O7Y8)!
|
||||||
@@ -6,11 +6,10 @@ If you need help with any aspect of Textual, let us know! We would be happy to h
|
|||||||
|
|
||||||
Report bugs via GitHub on the Textual [issues](https://github.com/Textualize/textual/issues) page. You can also post feature requests via GitHub issues, but see the [roadmap](./roadmap.md) first.
|
Report bugs via GitHub on the Textual [issues](https://github.com/Textualize/textual/issues) page. You can also post feature requests via GitHub issues, but see the [roadmap](./roadmap.md) first.
|
||||||
|
|
||||||
|
## Help with using Textual
|
||||||
|
|
||||||
|
You can seek help with using Textual [in the discussion area on GitHub](https://github.com/Textualize/textual/discussions).
|
||||||
|
|
||||||
## Discord Server
|
## Discord Server
|
||||||
|
|
||||||
For more realtime feedback or chat, join our discord server to connect with the [Textual community](https://discord.gg/Enf6Z3qhVr).
|
For more realtime feedback or chat, join our discord server to connect with the [Textual community](https://discord.gg/Enf6Z3qhVr).
|
||||||
|
|
||||||
## Forum
|
|
||||||
|
|
||||||
Visit the [Textual forum](https://community.textualize.io/) for Textual (and Rich) discussions.
|
|
||||||
|
|
||||||
|
|||||||
128
poetry.lock
generated
128
poetry.lock
generated
@@ -182,7 +182,7 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coverage"
|
name = "coverage"
|
||||||
version = "7.0.1"
|
version = "7.0.3"
|
||||||
description = "Code coverage measurement for Python"
|
description = "Code coverage measurement for Python"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -269,7 +269,7 @@ typing-extensions = {version = ">=3.7.4.3", markers = "python_version < \"3.8\""
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "griffe"
|
name = "griffe"
|
||||||
version = "0.25.2"
|
version = "0.25.3"
|
||||||
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
|
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -313,7 +313,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpx"
|
name = "httpx"
|
||||||
version = "0.23.2"
|
version = "0.23.3"
|
||||||
description = "The next generation HTTP client."
|
description = "The next generation HTTP client."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -333,7 +333,7 @@ socks = ["socksio (>=1.0.0,<2.0.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "identify"
|
name = "identify"
|
||||||
version = "2.5.11"
|
version = "2.5.12"
|
||||||
description = "File identification library for Python"
|
description = "File identification library for Python"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -524,7 +524,7 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mkdocstrings-python"
|
name = "mkdocstrings-python"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
description = "A Python handler for mkdocstrings."
|
description = "A Python handler for mkdocstrings."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -1178,57 +1178,57 @@ commonmark = [
|
|||||||
{file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
|
{file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
|
||||||
]
|
]
|
||||||
coverage = [
|
coverage = [
|
||||||
{file = "coverage-7.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b3695c4f4750bca943b3e1f74ad4be8d29e4aeab927d50772c41359107bd5d5c"},
|
{file = "coverage-7.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f7c51b6074a8a3063c341953dffe48fd6674f8e4b1d3c8aa8a91f58d6e716a8"},
|
||||||
{file = "coverage-7.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fa6a5a224b7f4cfb226f4fc55a57e8537fcc096f42219128c2c74c0e7d0953e1"},
|
{file = "coverage-7.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:628f47eaf66727fc986d3b190d6fa32f5e6b7754a243919d28bc0fd7974c449f"},
|
||||||
{file = "coverage-7.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74f70cd92669394eaf8d7756d1b195c8032cf7bbbdfce3bc489d4e15b3b8cf73"},
|
{file = "coverage-7.0.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89d5abf86c104de808108a25d171ad646c07eda96ca76c8b237b94b9c71e518"},
|
||||||
{file = "coverage-7.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b66bb21a23680dee0be66557dc6b02a3152ddb55edf9f6723fa4a93368f7158d"},
|
{file = "coverage-7.0.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75e43c6f4ea4d122dac389aabdf9d4f0e160770a75e63372f88005d90f5bcc80"},
|
||||||
{file = "coverage-7.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d87717959d4d0ee9db08a0f1d80d21eb585aafe30f9b0a54ecf779a69cb015f6"},
|
{file = "coverage-7.0.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49da0ff241827ebb52d5d6d5a36d33b455fa5e721d44689c95df99fd8db82437"},
|
||||||
{file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:854f22fa361d1ff914c7efa347398374cc7d567bdafa48ac3aa22334650dfba2"},
|
{file = "coverage-7.0.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0bce4ad5bdd0b02e177a085d28d2cea5fc57bb4ba2cead395e763e34cf934eb1"},
|
||||||
{file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1e414dc32ee5c3f36544ea466b6f52f28a7af788653744b8570d0bf12ff34bc0"},
|
{file = "coverage-7.0.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f79691335257d60951638dd43576b9bcd6f52baa5c1c2cd07a509bb003238372"},
|
||||||
{file = "coverage-7.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6c5ad996c6fa4d8ed669cfa1e8551348729d008a2caf81489ab9ea67cfbc7498"},
|
{file = "coverage-7.0.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5722269ed05fbdb94eef431787c66b66260ff3125d1a9afcc00facff8c45adf9"},
|
||||||
{file = "coverage-7.0.1-cp310-cp310-win32.whl", hash = "sha256:691571f31ace1837838b7e421d3a09a8c00b4aac32efacb4fc9bd0a5c647d25a"},
|
{file = "coverage-7.0.3-cp310-cp310-win32.whl", hash = "sha256:bdbda870e0fda7dd0fe7db7135ca226ec4c1ade8aa76e96614829b56ca491012"},
|
||||||
{file = "coverage-7.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:89caf4425fe88889e2973a8e9a3f6f5f9bbe5dd411d7d521e86428c08a873a4a"},
|
{file = "coverage-7.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:e56fae4292e216b8deeee38ace84557b9fa85b52db005368a275427cdabb8192"},
|
||||||
{file = "coverage-7.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:63d56165a7c76265468d7e0c5548215a5ba515fc2cba5232d17df97bffa10f6c"},
|
{file = "coverage-7.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b82343a5bc51627b9d606f0b6b6b9551db7b6311a5dd920fa52a94beae2e8959"},
|
||||||
{file = "coverage-7.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f943a3b2bc520102dd3e0bb465e1286e12c9a54f58accd71b9e65324d9c7c01"},
|
{file = "coverage-7.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fd0a8aa431f9b7ad9eb8264f55ef83cbb254962af3775092fb6e93890dea9ca2"},
|
||||||
{file = "coverage-7.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:830525361249dc4cd013652b0efad645a385707a5ae49350c894b67d23fbb07c"},
|
{file = "coverage-7.0.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:112cfead1bd22eada8a8db9ed387bd3e8be5528debc42b5d3c1f7da4ffaf9fb5"},
|
||||||
{file = "coverage-7.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd1b9c5adc066db699ccf7fa839189a649afcdd9e02cb5dc9d24e67e7922737d"},
|
{file = "coverage-7.0.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af87e906355fa42447be5c08c5d44e6e1c005bf142f303f726ddf5ed6e0c8a4d"},
|
||||||
{file = "coverage-7.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e00c14720b8b3b6c23b487e70bd406abafc976ddc50490f645166f111c419c39"},
|
{file = "coverage-7.0.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f30090e22a301952c5abd0e493a1c8358b4f0b368b49fa3e4568ed3ed68b8d1f"},
|
||||||
{file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6d55d840e1b8c0002fce66443e124e8581f30f9ead2e54fbf6709fb593181f2c"},
|
{file = "coverage-7.0.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ae871d09901911eedda1981ea6fd0f62a999107293cdc4c4fd612321c5b34745"},
|
||||||
{file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:66b18c3cf8bbab0cce0d7b9e4262dc830e93588986865a8c78ab2ae324b3ed56"},
|
{file = "coverage-7.0.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ed7c9debf7bfc63c9b9f8b595409237774ff4b061bf29fba6f53b287a2fdeab9"},
|
||||||
{file = "coverage-7.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:12a5aa77783d49e05439fbe6e6b427484f8a0f9f456b46a51d8aac022cfd024d"},
|
{file = "coverage-7.0.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:13121fa22dcd2c7b19c5161e3fd725692448f05377b788da4502a383573227b3"},
|
||||||
{file = "coverage-7.0.1-cp311-cp311-win32.whl", hash = "sha256:b77015d1cb8fe941be1222a5a8b4e3fbca88180cfa7e2d4a4e58aeabadef0ab7"},
|
{file = "coverage-7.0.3-cp311-cp311-win32.whl", hash = "sha256:037b51ee86bc600f99b3b957c20a172431c35c2ef9c1ca34bc813ab5b51fd9f5"},
|
||||||
{file = "coverage-7.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb992c47cb1e5bd6a01e97182400bcc2ba2077080a17fcd7be23aaa6e572e390"},
|
{file = "coverage-7.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:25fde928306034e8deecd5fc91a07432dcc282c8acb76749581a28963c9f4f3f"},
|
||||||
{file = "coverage-7.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e78e9dcbf4f3853d3ae18a8f9272111242531535ec9e1009fa8ec4a2b74557dc"},
|
{file = "coverage-7.0.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7e8b0642c38b3d3b3c01417643ccc645345b03c32a2e84ef93cdd6844d6fe530"},
|
||||||
{file = "coverage-7.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e60bef2e2416f15fdc05772bf87db06c6a6f9870d1db08fdd019fbec98ae24a9"},
|
{file = "coverage-7.0.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18b09811f849cc958d23f733a350a66b54a8de3fed1e6128ba55a5c97ffb6f65"},
|
||||||
{file = "coverage-7.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9823e4789ab70f3ec88724bba1a203f2856331986cd893dedbe3e23a6cfc1e4e"},
|
{file = "coverage-7.0.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:349d0b545520e8516f7b4f12373afc705d17d901e1de6a37a20e4ec9332b61f7"},
|
||||||
{file = "coverage-7.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9158f8fb06747ac17bd237930c4372336edc85b6e13bdc778e60f9d685c3ca37"},
|
{file = "coverage-7.0.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b38813eee5b4739f505d94247604c72eae626d5088a16dd77b08b8b1724ab3"},
|
||||||
{file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:486ee81fa694b4b796fc5617e376326a088f7b9729c74d9defa211813f3861e4"},
|
{file = "coverage-7.0.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ba9af1218fa01b1f11c72271bc7290b701d11ad4dbc2ae97c445ecacf6858dba"},
|
||||||
{file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1285648428a6101b5f41a18991c84f1c3959cee359e51b8375c5882fc364a13f"},
|
{file = "coverage-7.0.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c5648c7eec5cf1ba5db1cf2d6c10036a582d7f09e172990474a122e30c841361"},
|
||||||
{file = "coverage-7.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2c44fcfb3781b41409d0f060a4ed748537557de9362a8a9282182fafb7a76ab4"},
|
{file = "coverage-7.0.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d0df04495b76a885bfef009f45eebe8fe2fbf815ad7a83dabcf5aced62f33162"},
|
||||||
{file = "coverage-7.0.1-cp37-cp37m-win32.whl", hash = "sha256:d6814854c02cbcd9c873c0f3286a02e3ac1250625cca822ca6bc1018c5b19f1c"},
|
{file = "coverage-7.0.3-cp37-cp37m-win32.whl", hash = "sha256:af6cef3796b8068713a48dd67d258dc9a6e2ebc3bd4645bfac03a09672fa5d20"},
|
||||||
{file = "coverage-7.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f66460f17c9319ea4f91c165d46840314f0a7c004720b20be58594d162a441d8"},
|
{file = "coverage-7.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:62ef3800c4058844e2e3fa35faa9dd0ccde8a8aba6c763aae50342e00d4479d4"},
|
||||||
{file = "coverage-7.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9b373c9345c584bb4b5f5b8840df7f4ab48c4cbb7934b58d52c57020d911b856"},
|
{file = "coverage-7.0.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:acef7f3a3825a2d218a03dd02f5f3cc7f27aa31d882dd780191d1ad101120d74"},
|
||||||
{file = "coverage-7.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d3022c3007d3267a880b5adcf18c2a9bf1fc64469b394a804886b401959b8742"},
|
{file = "coverage-7.0.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a530663a361eb27375cec28aea5cd282089b5e4b022ae451c4c3493b026a68a5"},
|
||||||
{file = "coverage-7.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92651580bd46519067e36493acb394ea0607b55b45bd81dd4e26379ed1871f55"},
|
{file = "coverage-7.0.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c58cd6bb46dcb922e0d5792850aab5964433d511b3a020867650f8d930dde4f4"},
|
||||||
{file = "coverage-7.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cfc595d2af13856505631be072835c59f1acf30028d1c860b435c5fc9c15b69"},
|
{file = "coverage-7.0.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f918e9ef4c98f477a5458238dde2a1643aed956c7213873ab6b6b82e32b8ef61"},
|
||||||
{file = "coverage-7.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b4b3a4d9915b2be879aff6299c0a6129f3d08a775d5a061f503cf79571f73e4"},
|
{file = "coverage-7.0.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b865aa679bee7fbd1c55960940dbd3252621dd81468268786c67122bbd15343"},
|
||||||
{file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b6f22bb64cc39bcb883e5910f99a27b200fdc14cdd79df8696fa96b0005c9444"},
|
{file = "coverage-7.0.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:c5d9b480ebae60fc2cbc8d6865194136bc690538fa542ba58726433bed6e04cc"},
|
||||||
{file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:72d1507f152abacea81f65fee38e4ef3ac3c02ff8bc16f21d935fd3a8a4ad910"},
|
{file = "coverage-7.0.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:985ad2af5ec3dbb4fd75d5b0735752c527ad183455520055a08cf8d6794cabfc"},
|
||||||
{file = "coverage-7.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0a79137fc99815fff6a852c233628e735ec15903cfd16da0f229d9c4d45926ab"},
|
{file = "coverage-7.0.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ca15308ef722f120967af7474ba6a453e0f5b6f331251e20b8145497cf1bc14a"},
|
||||||
{file = "coverage-7.0.1-cp38-cp38-win32.whl", hash = "sha256:b3763e7fcade2ff6c8e62340af9277f54336920489ceb6a8cd6cc96da52fcc62"},
|
{file = "coverage-7.0.3-cp38-cp38-win32.whl", hash = "sha256:c1cee10662c25c94415bbb987f2ec0e6ba9e8fce786334b10be7e6a7ab958f69"},
|
||||||
{file = "coverage-7.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:09f6b5a8415b6b3e136d5fec62b552972187265cb705097bf030eb9d4ffb9b60"},
|
{file = "coverage-7.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:44d6a556de4418f1f3bfd57094b8c49f0408df5a433cf0d253eeb3075261c762"},
|
||||||
{file = "coverage-7.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:978258fec36c154b5e250d356c59af7d4c3ba02bef4b99cda90b6029441d797d"},
|
{file = "coverage-7.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e6dcc70a25cb95df0ae33dfc701de9b09c37f7dd9f00394d684a5b57257f8246"},
|
||||||
{file = "coverage-7.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:19ec666533f0f70a0993f88b8273057b96c07b9d26457b41863ccd021a043b9a"},
|
{file = "coverage-7.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bf76d79dfaea802f0f28f50153ffbc1a74ae1ee73e480baeda410b4f3e7ab25f"},
|
||||||
{file = "coverage-7.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfded268092a84605f1cc19e5c737f9ce630a8900a3589e9289622db161967e9"},
|
{file = "coverage-7.0.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88834e5d56d01c141c29deedacba5773fe0bed900b1edc957595a8a6c0da1c3c"},
|
||||||
{file = "coverage-7.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07bcfb1d8ac94af886b54e18a88b393f6a73d5959bb31e46644a02453c36e475"},
|
{file = "coverage-7.0.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef001a60e888f8741e42e5aa79ae55c91be73761e4df5e806efca1ddd62fd400"},
|
||||||
{file = "coverage-7.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b4a923cc7566bbc7ae2dfd0ba5a039b61d19c740f1373791f2ebd11caea59"},
|
{file = "coverage-7.0.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4959dc506be74e4963bd2c42f7b87d8e4b289891201e19ec551e64c6aa5441f8"},
|
||||||
{file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:aec2d1515d9d39ff270059fd3afbb3b44e6ec5758af73caf18991807138c7118"},
|
{file = "coverage-7.0.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b791beb17b32ac019a78cfbe6184f992b6273fdca31145b928ad2099435e2fcb"},
|
||||||
{file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c20cfebcc149a4c212f6491a5f9ff56f41829cd4f607b5be71bb2d530ef243b1"},
|
{file = "coverage-7.0.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b07651e3b9af8f1a092861d88b4c74d913634a7f1f2280fca0ad041ad84e9e96"},
|
||||||
{file = "coverage-7.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fd556ff16a57a070ce4f31c635953cc44e25244f91a0378c6e9bdfd40fdb249f"},
|
{file = "coverage-7.0.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:55e46fa4168ccb7497c9be78627fcb147e06f474f846a10d55feeb5108a24ef0"},
|
||||||
{file = "coverage-7.0.1-cp39-cp39-win32.whl", hash = "sha256:b9ea158775c7c2d3e54530a92da79496fb3fb577c876eec761c23e028f1e216c"},
|
{file = "coverage-7.0.3-cp39-cp39-win32.whl", hash = "sha256:e3f1cd1cd65695b1540b3cf7828d05b3515974a9d7c7530f762ac40f58a18161"},
|
||||||
{file = "coverage-7.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:d1991f1dd95eba69d2cd7708ff6c2bbd2426160ffc73c2b81f617a053ebcb1a8"},
|
{file = "coverage-7.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:d8249666c23683f74f8f93aeaa8794ac87cc61c40ff70374a825f3352a4371dc"},
|
||||||
{file = "coverage-7.0.1-pp37.pp38.pp39-none-any.whl", hash = "sha256:3dd4ee135e08037f458425b8842d24a95a0961831a33f89685ff86b77d378f89"},
|
{file = "coverage-7.0.3-pp37.pp38.pp39-none-any.whl", hash = "sha256:b1ffc8f58b81baed3f8962e28c30d99442079b82ce1ec836a1f67c0accad91c1"},
|
||||||
{file = "coverage-7.0.1.tar.gz", hash = "sha256:a4a574a19eeb67575a5328a5760bbbb737faa685616586a9f9da4281f940109c"},
|
{file = "coverage-7.0.3.tar.gz", hash = "sha256:d5be4e93acce64f516bf4fd239c0e6118fc913c93fa1a3f52d15bdcc60d97b2d"},
|
||||||
]
|
]
|
||||||
distlib = [
|
distlib = [
|
||||||
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
|
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
|
||||||
@@ -1331,8 +1331,8 @@ gitpython = [
|
|||||||
{file = "GitPython-3.1.30.tar.gz", hash = "sha256:769c2d83e13f5d938b7688479da374c4e3d49f71549aaf462b646db9602ea6f8"},
|
{file = "GitPython-3.1.30.tar.gz", hash = "sha256:769c2d83e13f5d938b7688479da374c4e3d49f71549aaf462b646db9602ea6f8"},
|
||||||
]
|
]
|
||||||
griffe = [
|
griffe = [
|
||||||
{file = "griffe-0.25.2-py3-none-any.whl", hash = "sha256:0868da415c5f43fe186705c041d98b69523c24a6504e841031373eacfdd7ec05"},
|
{file = "griffe-0.25.3-py3-none-any.whl", hash = "sha256:c98e8471a4fc7675a7989f45563a9f7ccbfdfb1713725526d69dec1bbdcda74a"},
|
||||||
{file = "griffe-0.25.2.tar.gz", hash = "sha256:555707b3417355e015d837845522cb38ee4ffcec485427868648eafacabe142e"},
|
{file = "griffe-0.25.3.tar.gz", hash = "sha256:a71f156851649b3f0bdad6eb6bf7d7ac70e720a30da9f2d5a60e042480e92c03"},
|
||||||
]
|
]
|
||||||
h11 = [
|
h11 = [
|
||||||
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||||
@@ -1343,12 +1343,12 @@ httpcore = [
|
|||||||
{file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"},
|
{file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"},
|
||||||
]
|
]
|
||||||
httpx = [
|
httpx = [
|
||||||
{file = "httpx-0.23.2-py3-none-any.whl", hash = "sha256:106cded342a44e443060fab70ef327139248c61939e77d73964560c8d8b57069"},
|
{file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"},
|
||||||
{file = "httpx-0.23.2.tar.gz", hash = "sha256:e824a6fa18ffaa6423c6f3a32d5096fc15bd8dff43663a223f06242fc69451a8"},
|
{file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"},
|
||||||
]
|
]
|
||||||
identify = [
|
identify = [
|
||||||
{file = "identify-2.5.11-py2.py3-none-any.whl", hash = "sha256:e7db36b772b188099616aaf2accbee122949d1c6a1bac4f38196720d6f9f06db"},
|
{file = "identify-2.5.12-py2.py3-none-any.whl", hash = "sha256:e8a400c3062d980243d27ce10455a52832205649bbcaf27ffddb3dfaaf477bad"},
|
||||||
{file = "identify-2.5.11.tar.gz", hash = "sha256:14b7076b29c99b1b0b8b08e96d448c7b877a9b07683cd8cfda2ea06af85ffa1c"},
|
{file = "identify-2.5.12.tar.gz", hash = "sha256:0bc96b09c838310b6fcfcc61f78a981ea07f94836ef6ef553da5bb5d4745d662"},
|
||||||
]
|
]
|
||||||
idna = [
|
idna = [
|
||||||
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
|
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
|
||||||
@@ -1441,8 +1441,8 @@ mkdocstrings = [
|
|||||||
{file = "mkdocstrings-0.19.1.tar.gz", hash = "sha256:d1037cacb4b522c1e8c164ed5d00d724a82e49dcee0af80db8fb67b384faeef9"},
|
{file = "mkdocstrings-0.19.1.tar.gz", hash = "sha256:d1037cacb4b522c1e8c164ed5d00d724a82e49dcee0af80db8fb67b384faeef9"},
|
||||||
]
|
]
|
||||||
mkdocstrings-python = [
|
mkdocstrings-python = [
|
||||||
{file = "mkdocstrings-python-0.8.2.tar.gz", hash = "sha256:b22528b7a7a0589d007eced019d97ad14de4eba4b2b9ba6a013bb66edc74ab43"},
|
{file = "mkdocstrings-python-0.8.3.tar.gz", hash = "sha256:9ae473f6dc599339b09eee17e4d2b05d6ac0ec29860f3fc9b7512d940fc61adf"},
|
||||||
{file = "mkdocstrings_python-0.8.2-py3-none-any.whl", hash = "sha256:213d9592e66e084a9bd2fa4956d6294a3487c6dc9cc45164058d6317249b7b6e"},
|
{file = "mkdocstrings_python-0.8.3-py3-none-any.whl", hash = "sha256:4e6e1cd6f37a785de0946ced6eb846eb2f5d891ac1cc2c7b832943d3529087a7"},
|
||||||
]
|
]
|
||||||
msgpack = [
|
msgpack = [
|
||||||
{file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250"},
|
{file = "msgpack-1.0.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4ab251d229d10498e9a2f3b1e68ef64cb393394ec477e3370c457f9430ce9250"},
|
||||||
|
|||||||
@@ -253,8 +253,8 @@ class Compositor:
|
|||||||
# Keep a copy of the old map because we're going to compare it with the update
|
# Keep a copy of the old map because we're going to compare it with the update
|
||||||
old_map = self.map.copy()
|
old_map = self.map.copy()
|
||||||
old_widgets = old_map.keys()
|
old_widgets = old_map.keys()
|
||||||
map, widgets = self._arrange_root(parent, size)
|
|
||||||
|
|
||||||
|
map, widgets = self._arrange_root(parent, size)
|
||||||
new_widgets = map.keys()
|
new_widgets = map.keys()
|
||||||
|
|
||||||
# Newly visible widgets
|
# Newly visible widgets
|
||||||
@@ -266,20 +266,21 @@ class Compositor:
|
|||||||
self.map = map
|
self.map = map
|
||||||
self.widgets = widgets
|
self.widgets = widgets
|
||||||
|
|
||||||
screen = size.region
|
# Contains widgets + geometry for every widget that changed (added, removed, or updated)
|
||||||
|
changes = map.items() ^ old_map.items()
|
||||||
|
|
||||||
|
# Widgets in both new and old
|
||||||
|
common_widgets = old_widgets & new_widgets
|
||||||
|
|
||||||
# Widgets with changed size
|
# Widgets with changed size
|
||||||
resized_widgets = {
|
resized_widgets = {
|
||||||
widget
|
widget
|
||||||
for widget, (region, *_) in map.items()
|
for widget, (region, *_) in changes
|
||||||
if widget in old_widgets and old_map[widget].region.size != region.size
|
if (widget in common_widgets and old_map[widget].region[2:] != region[2:])
|
||||||
}
|
}
|
||||||
|
|
||||||
# Gets pairs of tuples of (Widget, MapGeometry) which have changed
|
screen_region = size.region
|
||||||
# i.e. if something is moved / deleted / added
|
if screen_region not in self._dirty_regions:
|
||||||
|
|
||||||
if screen not in self._dirty_regions:
|
|
||||||
changes = map.items() ^ old_map.items()
|
|
||||||
regions = {
|
regions = {
|
||||||
region
|
region
|
||||||
for region in (
|
for region in (
|
||||||
@@ -346,6 +347,7 @@ class Compositor:
|
|||||||
layer_order: int,
|
layer_order: int,
|
||||||
clip: Region,
|
clip: Region,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
|
_MapGeometry=MapGeometry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Called recursively to place a widget and its children in the map.
|
"""Called recursively to place a widget and its children in the map.
|
||||||
|
|
||||||
@@ -392,50 +394,57 @@ class Compositor:
|
|||||||
)
|
)
|
||||||
widgets.update(arranged_widgets)
|
widgets.update(arranged_widgets)
|
||||||
|
|
||||||
# An offset added to all placements
|
if placements:
|
||||||
placement_offset = container_region.offset
|
# An offset added to all placements
|
||||||
placement_scroll_offset = placement_offset - widget.scroll_offset
|
placement_offset = container_region.offset
|
||||||
|
placement_scroll_offset = (
|
||||||
|
placement_offset - widget.scroll_offset
|
||||||
|
)
|
||||||
|
|
||||||
_layers = widget.layers
|
_layers = widget.layers
|
||||||
layers_to_index = {
|
layers_to_index = {
|
||||||
layer_name: index for index, layer_name in enumerate(_layers)
|
layer_name: index
|
||||||
}
|
for index, layer_name in enumerate(_layers)
|
||||||
get_layer_index = layers_to_index.get
|
}
|
||||||
|
get_layer_index = layers_to_index.get
|
||||||
|
|
||||||
# Add all the widgets
|
# Add all the widgets
|
||||||
for sub_region, margin, sub_widget, z, fixed in reversed(
|
for sub_region, margin, sub_widget, z, fixed in reversed(
|
||||||
placements
|
placements
|
||||||
):
|
):
|
||||||
# Combine regions with children to calculate the "virtual size"
|
# Combine regions with children to calculate the "virtual size"
|
||||||
if fixed:
|
if fixed:
|
||||||
widget_region = sub_region + placement_offset
|
widget_region = sub_region + placement_offset
|
||||||
else:
|
else:
|
||||||
total_region = total_region.union(
|
total_region = total_region.union(
|
||||||
sub_region.grow(spacing + margin)
|
sub_region.grow(spacing + margin)
|
||||||
|
)
|
||||||
|
widget_region = sub_region + placement_scroll_offset
|
||||||
|
|
||||||
|
widget_order = (
|
||||||
|
*order,
|
||||||
|
get_layer_index(sub_widget.layer, 0),
|
||||||
|
z,
|
||||||
|
layer_order,
|
||||||
)
|
)
|
||||||
widget_region = sub_region + placement_scroll_offset
|
|
||||||
|
|
||||||
widget_order = order + (
|
add_widget(
|
||||||
(get_layer_index(sub_widget.layer, 0), z, layer_order),
|
sub_widget,
|
||||||
)
|
sub_region,
|
||||||
|
widget_region,
|
||||||
add_widget(
|
widget_order,
|
||||||
sub_widget,
|
layer_order,
|
||||||
sub_region,
|
sub_clip,
|
||||||
widget_region,
|
visible,
|
||||||
widget_order,
|
)
|
||||||
layer_order,
|
layer_order -= 1
|
||||||
sub_clip,
|
|
||||||
visible,
|
|
||||||
)
|
|
||||||
layer_order -= 1
|
|
||||||
|
|
||||||
if visible:
|
if visible:
|
||||||
# Add any scrollbars
|
# Add any scrollbars
|
||||||
for chrome_widget, chrome_region in widget._arrange_scrollbars(
|
for chrome_widget, chrome_region in widget._arrange_scrollbars(
|
||||||
container_region
|
container_region
|
||||||
):
|
):
|
||||||
map[chrome_widget] = MapGeometry(
|
map[chrome_widget] = _MapGeometry(
|
||||||
chrome_region + layout_offset,
|
chrome_region + layout_offset,
|
||||||
order,
|
order,
|
||||||
clip,
|
clip,
|
||||||
@@ -444,7 +453,7 @@ class Compositor:
|
|||||||
chrome_region,
|
chrome_region,
|
||||||
)
|
)
|
||||||
|
|
||||||
map[widget] = MapGeometry(
|
map[widget] = _MapGeometry(
|
||||||
region + layout_offset,
|
region + layout_offset,
|
||||||
order,
|
order,
|
||||||
clip,
|
clip,
|
||||||
@@ -455,7 +464,7 @@ class Compositor:
|
|||||||
|
|
||||||
elif visible:
|
elif visible:
|
||||||
# Add the widget to the map
|
# Add the widget to the map
|
||||||
map[widget] = MapGeometry(
|
map[widget] = _MapGeometry(
|
||||||
region + layout_offset,
|
region + layout_offset,
|
||||||
order,
|
order,
|
||||||
clip,
|
clip,
|
||||||
|
|||||||
@@ -14,6 +14,56 @@ from .css.types import AlignHorizontal, AlignVertical
|
|||||||
from .geometry import Size
|
from .geometry import Size
|
||||||
|
|
||||||
|
|
||||||
|
class NoCellPositionForIndex(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def index_to_cell_position(segments: Iterable[Segment], index: int) -> int:
|
||||||
|
"""Given a character index, return the cell position of that character within
|
||||||
|
an Iterable of Segments. This is the sum of the cell lengths of all the characters
|
||||||
|
*before* the character at `index`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
segments (Iterable[Segment]): The segments to find the cell position within.
|
||||||
|
index (int): The index to convert into a cell position.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The cell position of the character at `index`.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NoCellPositionForIndex: If the supplied index doesn't fall within the given segments.
|
||||||
|
"""
|
||||||
|
if not segments:
|
||||||
|
raise NoCellPositionForIndex
|
||||||
|
|
||||||
|
if index == 0:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
cell_position_end = 0
|
||||||
|
segment_length = 0
|
||||||
|
segment_end_index = 0
|
||||||
|
segment_cell_length = 0
|
||||||
|
text = ""
|
||||||
|
iter_segments = iter(segments)
|
||||||
|
try:
|
||||||
|
while segment_end_index < index:
|
||||||
|
segment = next(iter_segments)
|
||||||
|
text = segment.text
|
||||||
|
segment_length = len(text)
|
||||||
|
segment_cell_length = cell_len(text)
|
||||||
|
cell_position_end += segment_cell_length
|
||||||
|
segment_end_index += segment_length
|
||||||
|
except StopIteration:
|
||||||
|
raise NoCellPositionForIndex
|
||||||
|
|
||||||
|
# Check how far into this segment the target index is
|
||||||
|
segment_index_start = segment_end_index - segment_length
|
||||||
|
index_within_segment = index - segment_index_start
|
||||||
|
segment_cell_start = cell_position_end - segment_cell_length
|
||||||
|
|
||||||
|
return segment_cell_start + cell_len(text[:index_within_segment])
|
||||||
|
|
||||||
|
|
||||||
def line_crop(
|
def line_crop(
|
||||||
segments: list[Segment], start: int, end: int, total: int
|
segments: list[Segment], start: int, end: int, total: int
|
||||||
) -> list[Segment]:
|
) -> list[Segment]:
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from concurrent.futures import Future
|
||||||
|
from functools import partial
|
||||||
import inspect
|
import inspect
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
@@ -18,6 +20,7 @@ from time import perf_counter
|
|||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Any,
|
Any,
|
||||||
|
Awaitable,
|
||||||
Callable,
|
Callable,
|
||||||
Generic,
|
Generic,
|
||||||
Iterable,
|
Iterable,
|
||||||
@@ -206,6 +209,8 @@ class _WriterThread(threading.Thread):
|
|||||||
|
|
||||||
CSSPathType = Union[str, PurePath, List[Union[str, PurePath]], None]
|
CSSPathType = Union[str, PurePath, List[Union[str, PurePath]], None]
|
||||||
|
|
||||||
|
CallThreadReturnType = TypeVar("CallThreadReturnType")
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
class App(Generic[ReturnType], DOMNode):
|
class App(Generic[ReturnType], DOMNode):
|
||||||
@@ -353,6 +358,8 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
else:
|
else:
|
||||||
self.devtools = DevtoolsClient()
|
self.devtools = DevtoolsClient()
|
||||||
|
|
||||||
|
self._loop: asyncio.AbstractEventLoop | None = None
|
||||||
|
self._thread_id: int = 0
|
||||||
self._return_value: ReturnType | None = None
|
self._return_value: ReturnType | None = None
|
||||||
self._exit = False
|
self._exit = False
|
||||||
|
|
||||||
@@ -604,6 +611,51 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
except Exception as error:
|
except Exception as error:
|
||||||
self._handle_exception(error)
|
self._handle_exception(error)
|
||||||
|
|
||||||
|
def call_from_thread(
|
||||||
|
self,
|
||||||
|
callback: Callable[..., CallThreadReturnType | Awaitable[CallThreadReturnType]],
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
|
) -> CallThreadReturnType:
|
||||||
|
"""Run a callback from another thread.
|
||||||
|
|
||||||
|
Like asyncio apps in general, Textual apps are not thread-safe. If you call methods
|
||||||
|
or set attributes on Textual objects from a thread, you may get unpredictable results.
|
||||||
|
|
||||||
|
This method will ensure that your code is ran within the correct context.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callback (Callable): A callable to run.
|
||||||
|
*args: Arguments to the callback.
|
||||||
|
**kwargs: Keyword arguments for the callback.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If the app isn't running or if this method is called from the same
|
||||||
|
thread where the app is running.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._loop is None:
|
||||||
|
raise RuntimeError("App is not running")
|
||||||
|
|
||||||
|
if self._thread_id == threading.get_ident():
|
||||||
|
raise RuntimeError(
|
||||||
|
"The `call_from_thread` method must run in a different thread from the app"
|
||||||
|
)
|
||||||
|
|
||||||
|
callback_with_args = partial(callback, *args, **kwargs)
|
||||||
|
|
||||||
|
async def run_callback() -> CallThreadReturnType:
|
||||||
|
"""Run the callback, set the result or error on the future."""
|
||||||
|
self._set_active()
|
||||||
|
return await invoke(callback_with_args)
|
||||||
|
|
||||||
|
# Post the message to the main loop
|
||||||
|
future: Future[CallThreadReturnType] = asyncio.run_coroutine_threadsafe(
|
||||||
|
run_callback(), loop=self._loop
|
||||||
|
)
|
||||||
|
result = future.result()
|
||||||
|
return result
|
||||||
|
|
||||||
def action_toggle_dark(self) -> None:
|
def action_toggle_dark(self) -> None:
|
||||||
"""Action to toggle dark mode."""
|
"""Action to toggle dark mode."""
|
||||||
self.dark = not self.dark
|
self.dark = not self.dark
|
||||||
@@ -874,11 +926,17 @@ class App(Generic[ReturnType], DOMNode):
|
|||||||
|
|
||||||
async def run_app() -> None:
|
async def run_app() -> None:
|
||||||
"""Run the app."""
|
"""Run the app."""
|
||||||
await self.run_async(
|
self._loop = asyncio.get_running_loop()
|
||||||
headless=headless,
|
self._thread_id = threading.get_ident()
|
||||||
size=size,
|
try:
|
||||||
auto_pilot=auto_pilot,
|
await self.run_async(
|
||||||
)
|
headless=headless,
|
||||||
|
size=size,
|
||||||
|
auto_pilot=auto_pilot,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
self._loop = None
|
||||||
|
self._thread_id = 0
|
||||||
|
|
||||||
if _ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED:
|
if _ASYNCIO_GET_EVENT_LOOP_IS_DEPRECATED:
|
||||||
# N.B. This doesn't work with Python<3.10, as we end up with 2 event loops:
|
# N.B. This doesn't work with Python<3.10, as we end up with 2 event loops:
|
||||||
|
|||||||
@@ -684,6 +684,7 @@ class Region(NamedTuple):
|
|||||||
)
|
)
|
||||||
return new_region
|
return new_region
|
||||||
|
|
||||||
|
@lru_cache(maxsize=4096)
|
||||||
def grow(self, margin: tuple[int, int, int, int]) -> Region:
|
def grow(self, margin: tuple[int, int, int, int]) -> Region:
|
||||||
"""Grow a region by adding spacing.
|
"""Grow a region by adding spacing.
|
||||||
|
|
||||||
@@ -704,6 +705,7 @@ class Region(NamedTuple):
|
|||||||
height=max(0, height + top + bottom),
|
height=max(0, height + top + bottom),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@lru_cache(maxsize=4096)
|
||||||
def shrink(self, margin: tuple[int, int, int, int]) -> Region:
|
def shrink(self, margin: tuple[int, int, int, int]) -> Region:
|
||||||
"""Shrink a region by subtracting spacing.
|
"""Shrink a region by subtracting spacing.
|
||||||
|
|
||||||
@@ -713,7 +715,8 @@ class Region(NamedTuple):
|
|||||||
Returns:
|
Returns:
|
||||||
Region: The new, smaller region.
|
Region: The new, smaller region.
|
||||||
"""
|
"""
|
||||||
|
if not any(margin):
|
||||||
|
return self
|
||||||
top, right, bottom, left = margin
|
top, right, bottom, left = margin
|
||||||
x, y, width, height = self
|
x, y, width, height = self
|
||||||
return Region(
|
return Region(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from rich.style import Style
|
|||||||
|
|
||||||
from ._cache import FIFOCache
|
from ._cache import FIFOCache
|
||||||
from ._filter import LineFilter
|
from ._filter import LineFilter
|
||||||
|
from ._segment_tools import index_to_cell_position
|
||||||
|
|
||||||
|
|
||||||
@rich.repr.auto
|
@rich.repr.auto
|
||||||
@@ -68,6 +69,19 @@ class Strip:
|
|||||||
"""
|
"""
|
||||||
return [cls(segments, cell_length) for segments in lines]
|
return [cls(segments, cell_length) for segments in lines]
|
||||||
|
|
||||||
|
def index_to_cell_position(self, index: int) -> int:
|
||||||
|
"""Given a character index, return the cell position of that character.
|
||||||
|
This is the sum of the cell lengths of all the characters *before* the character
|
||||||
|
at `index`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
index (int): The index to convert.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The cell position of the character at `index`.
|
||||||
|
"""
|
||||||
|
return index_to_cell_position(self._segments, index)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cell_length(self) -> int:
|
def cell_length(self) -> int:
|
||||||
"""Get the number of cells required to render this object."""
|
"""Get the number of cells required to render this object."""
|
||||||
|
|||||||
@@ -802,15 +802,17 @@ class Widget(DOMNode):
|
|||||||
if self.auto_links:
|
if self.auto_links:
|
||||||
self.highlight_link_id = hover_style.link_id
|
self.highlight_link_id = hover_style.link_id
|
||||||
|
|
||||||
def watch_scroll_x(self, new_value: float) -> None:
|
def watch_scroll_x(self, old_value: float, new_value: float) -> None:
|
||||||
if self.show_horizontal_scrollbar:
|
if self.show_horizontal_scrollbar:
|
||||||
self.horizontal_scrollbar.position = int(new_value)
|
self.horizontal_scrollbar.position = round(new_value)
|
||||||
self.refresh(layout=True, repaint=False)
|
if round(old_value) != round(new_value):
|
||||||
|
self._refresh_scroll()
|
||||||
|
|
||||||
def watch_scroll_y(self, new_value: float) -> None:
|
def watch_scroll_y(self, old_value: float, new_value: float) -> None:
|
||||||
if self.show_vertical_scrollbar:
|
if self.show_vertical_scrollbar:
|
||||||
self.vertical_scrollbar.position = int(new_value)
|
self.vertical_scrollbar.position = round(new_value)
|
||||||
self.refresh(layout=True, repaint=False)
|
if round(old_value) != round(new_value):
|
||||||
|
self._refresh_scroll()
|
||||||
|
|
||||||
def validate_scroll_x(self, value: float) -> float:
|
def validate_scroll_x(self, value: float) -> float:
|
||||||
return clamp(value, 0, self.max_scroll_x)
|
return clamp(value, 0, self.max_scroll_x)
|
||||||
@@ -1147,7 +1149,7 @@ class Widget(DOMNode):
|
|||||||
Returns:
|
Returns:
|
||||||
Offset: Offset a container has been scrolled by.
|
Offset: Offset a container has been scrolled by.
|
||||||
"""
|
"""
|
||||||
return Offset(int(self.scroll_x), int(self.scroll_y))
|
return Offset(round(self.scroll_x), round(self.scroll_y))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_transparent(self) -> bool:
|
def is_transparent(self) -> bool:
|
||||||
@@ -2155,8 +2157,16 @@ class Widget(DOMNode):
|
|||||||
event._set_forwarded()
|
event._set_forwarded()
|
||||||
await self.post_message(event)
|
await self.post_message(event)
|
||||||
|
|
||||||
|
def _refresh_scroll(self) -> None:
|
||||||
|
"""Refreshes the scroll position."""
|
||||||
|
self._layout_required = True
|
||||||
|
self.check_idle()
|
||||||
|
|
||||||
def refresh(
|
def refresh(
|
||||||
self, *regions: Region, repaint: bool = True, layout: bool = False
|
self,
|
||||||
|
*regions: Region,
|
||||||
|
repaint: bool = True,
|
||||||
|
layout: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initiate a refresh of the widget.
|
"""Initiate a refresh of the widget.
|
||||||
|
|
||||||
|
|||||||
53
tests/test_concurrency.py
Normal file
53
tests/test_concurrency.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from threading import Thread
|
||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from textual.widgets import TextLog
|
||||||
|
|
||||||
|
|
||||||
|
def test_call_from_thread_app_not_running():
|
||||||
|
app = App()
|
||||||
|
|
||||||
|
# Should fail if app is not running
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
app.call_from_thread(print)
|
||||||
|
|
||||||
|
|
||||||
|
def test_call_from_thread():
|
||||||
|
"""Test the call_from_thread method."""
|
||||||
|
|
||||||
|
class BackgroundThread(Thread):
|
||||||
|
"""A background thread which will modify app in some way."""
|
||||||
|
|
||||||
|
def __init__(self, app: App) -> None:
|
||||||
|
self.app = app
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
def write_stuff(text: str) -> None:
|
||||||
|
"""Write stuff to a widget."""
|
||||||
|
self.app.query_one(TextLog).write(text)
|
||||||
|
|
||||||
|
self.app.call_from_thread(write_stuff, "Hello")
|
||||||
|
# Exit the app with a code we can assert
|
||||||
|
self.app.call_from_thread(self.app.exit, 123)
|
||||||
|
|
||||||
|
class ThreadTestApp(App):
|
||||||
|
"""Trivial app with a single widget."""
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield TextLog()
|
||||||
|
|
||||||
|
def on_ready(self) -> None:
|
||||||
|
"""Launch a thread which will modify the app."""
|
||||||
|
try:
|
||||||
|
self.call_from_thread(print)
|
||||||
|
except RuntimeError as error:
|
||||||
|
# Calling this from the same thread as the app is an error
|
||||||
|
self._runtime_error = error
|
||||||
|
BackgroundThread(self).start()
|
||||||
|
|
||||||
|
app = ThreadTestApp()
|
||||||
|
result = app.run(headless=True, size=(80, 24))
|
||||||
|
assert isinstance(app._runtime_error, RuntimeError)
|
||||||
|
assert result == 123
|
||||||
@@ -24,7 +24,7 @@ def test_non_empty_immutable_sequence() -> None:
|
|||||||
|
|
||||||
def test_no_assign_to_immutable_sequence() -> None:
|
def test_no_assign_to_immutable_sequence() -> None:
|
||||||
"""It should not be possible to assign into an immutable sequence."""
|
"""It should not be possible to assign into an immutable sequence."""
|
||||||
tester = wrap([1,2,3,4,5])
|
tester = wrap([1, 2, 3, 4, 5])
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
tester[0] = 23
|
tester[0] = 23
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
@@ -33,7 +33,7 @@ def test_no_assign_to_immutable_sequence() -> None:
|
|||||||
|
|
||||||
def test_no_del_from_iummutable_sequence() -> None:
|
def test_no_del_from_iummutable_sequence() -> None:
|
||||||
"""It should not be possible delete an item from an immutable sequence."""
|
"""It should not be possible delete an item from an immutable sequence."""
|
||||||
tester = wrap([1,2,3,4,5])
|
tester = wrap([1, 2, 3, 4, 5])
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
del tester[0]
|
del tester[0]
|
||||||
|
|
||||||
@@ -46,23 +46,23 @@ def test_get_item_from_immutable_sequence() -> None:
|
|||||||
|
|
||||||
def test_get_slice_from_immutable_sequence() -> None:
|
def test_get_slice_from_immutable_sequence() -> None:
|
||||||
"""It should be possible to get a slice from an immutable sequence."""
|
"""It should be possible to get a slice from an immutable sequence."""
|
||||||
assert list(wrap(range(10))[0:2]) == [0,1]
|
assert list(wrap(range(10))[0:2]) == [0, 1]
|
||||||
assert list(wrap(range(10))[0:-1]) == [0,1,2,3,4,5,6,7,8]
|
assert list(wrap(range(10))[0:-1]) == [0, 1, 2, 3, 4, 5, 6, 7, 8]
|
||||||
|
|
||||||
|
|
||||||
def test_immutable_sequence_contains() -> None:
|
def test_immutable_sequence_contains() -> None:
|
||||||
"""It should be possible to see if an immutable sequence contains a value."""
|
"""It should be possible to see if an immutable sequence contains a value."""
|
||||||
tester = wrap([1,2,3,4,5])
|
tester = wrap([1, 2, 3, 4, 5])
|
||||||
assert 1 in tester
|
assert 1 in tester
|
||||||
assert 11 not in tester
|
assert 11 not in tester
|
||||||
|
|
||||||
|
|
||||||
def test_immutable_sequence_index() -> None:
|
def test_immutable_sequence_index() -> None:
|
||||||
tester = wrap([1,2,3,4,5])
|
tester = wrap([1, 2, 3, 4, 5])
|
||||||
assert tester.index(1) == 0
|
assert tester.index(1) == 0
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
_ = tester.index(11)
|
_ = tester.index(11)
|
||||||
|
|
||||||
|
|
||||||
def test_reverse_immutable_sequence() -> None:
|
def test_reverse_immutable_sequence() -> None:
|
||||||
assert list(reversed(wrap([1,2]))) == [2,1]
|
assert list(reversed(wrap([1, 2]))) == [2, 1]
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import pytest
|
||||||
from rich.segment import Segment
|
from rich.segment import Segment
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
|
from textual._segment_tools import NoCellPositionForIndex
|
||||||
from textual.strip import Strip
|
from textual.strip import Strip
|
||||||
from textual._filter import Monochrome
|
from textual._filter import Monochrome
|
||||||
|
|
||||||
@@ -62,9 +64,7 @@ def test_eq():
|
|||||||
|
|
||||||
|
|
||||||
def test_adjust_cell_length():
|
def test_adjust_cell_length():
|
||||||
|
|
||||||
for repeat in range(3):
|
for repeat in range(3):
|
||||||
|
|
||||||
assert Strip([]).adjust_cell_length(3) == Strip([Segment(" ")])
|
assert Strip([]).adjust_cell_length(3) == Strip([Segment(" ")])
|
||||||
assert Strip([Segment("f")]).adjust_cell_length(3) == Strip(
|
assert Strip([Segment("f")]).adjust_cell_length(3) == Strip(
|
||||||
[Segment("f"), Segment(" ")]
|
[Segment("f"), Segment(" ")]
|
||||||
@@ -119,9 +119,7 @@ def test_style_links():
|
|||||||
|
|
||||||
|
|
||||||
def test_crop():
|
def test_crop():
|
||||||
|
|
||||||
for repeat in range(3):
|
for repeat in range(3):
|
||||||
|
|
||||||
assert Strip([Segment("foo")]).crop(0, 3) == Strip([Segment("foo")])
|
assert Strip([Segment("foo")]).crop(0, 3) == Strip([Segment("foo")])
|
||||||
assert Strip([Segment("foo")]).crop(0, 2) == Strip([Segment("fo")])
|
assert Strip([Segment("foo")]).crop(0, 2) == Strip([Segment("fo")])
|
||||||
assert Strip([Segment("foo")]).crop(0, 1) == Strip([Segment("f")])
|
assert Strip([Segment("foo")]).crop(0, 1) == Strip([Segment("f")])
|
||||||
@@ -136,10 +134,42 @@ def test_crop():
|
|||||||
|
|
||||||
|
|
||||||
def test_divide():
|
def test_divide():
|
||||||
|
|
||||||
for repeat in range(3):
|
for repeat in range(3):
|
||||||
|
|
||||||
assert Strip([Segment("foo")]).divide([1, 2]) == [
|
assert Strip([Segment("foo")]).divide([1, 2]) == [
|
||||||
Strip([Segment("f")]),
|
Strip([Segment("f")]),
|
||||||
Strip([Segment("o")]),
|
Strip([Segment("o")]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"index,cell_position",
|
||||||
|
[
|
||||||
|
(0, 0),
|
||||||
|
(1, 1),
|
||||||
|
(2, 2),
|
||||||
|
(3, 3),
|
||||||
|
(4, 4),
|
||||||
|
(5, 6),
|
||||||
|
(6, 8),
|
||||||
|
(7, 10),
|
||||||
|
(8, 11),
|
||||||
|
(9, 12),
|
||||||
|
(10, 13),
|
||||||
|
(11, 14),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_index_to_cell_position(index, cell_position):
|
||||||
|
strip = Strip([Segment("ab"), Segment("cd日本語ef"), Segment("gh")])
|
||||||
|
assert cell_position == strip.index_to_cell_position(index)
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_cell_position_no_segments():
|
||||||
|
strip = Strip([])
|
||||||
|
with pytest.raises(NoCellPositionForIndex):
|
||||||
|
strip.index_to_cell_position(2)
|
||||||
|
|
||||||
|
|
||||||
|
def test_index_cell_position_index_too_large():
|
||||||
|
strip = Strip([Segment("abcdef"), Segment("ghi")])
|
||||||
|
with pytest.raises(NoCellPositionForIndex):
|
||||||
|
strip.index_to_cell_position(100)
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from textual.widgets import Tree, TreeNode
|
from textual.widgets import Tree, TreeNode
|
||||||
|
|
||||||
def label_of(node: TreeNode[None]):
|
|
||||||
"""Get the label of a node.
|
|
||||||
|
|
||||||
TODO: This is just a helper function to reduce the number of type
|
def label_of(node: TreeNode[None]):
|
||||||
errors, which can and will be remove once this code is merged with a
|
"""Get the label of a node as a string"""
|
||||||
version of main that also has the TreeNode.label PR merged.
|
return str(node.label)
|
||||||
"""
|
|
||||||
return str(node._label)
|
|
||||||
|
|
||||||
|
|
||||||
def test_tree_node_children() -> None:
|
def test_tree_node_children() -> None:
|
||||||
"""A node's children property should act like an immutable list."""
|
"""A node's children property should act like an immutable list."""
|
||||||
CHILDREN=23
|
CHILDREN = 23
|
||||||
tree = Tree[None]("Root")
|
tree = Tree[None]("Root")
|
||||||
for child in range(CHILDREN):
|
for child in range(CHILDREN):
|
||||||
tree.root.add(str(child))
|
tree.root.add(str(child))
|
||||||
assert len(tree.root.children)==CHILDREN
|
assert len(tree.root.children) == CHILDREN
|
||||||
for child in range(CHILDREN):
|
for child in range(CHILDREN):
|
||||||
assert label_of(tree.root.children[child]) == str(child)
|
assert label_of(tree.root.children[child]) == str(child)
|
||||||
assert label_of(tree.root.children[0]) == "0"
|
assert label_of(tree.root.children[0]) == "0"
|
||||||
assert label_of(tree.root.children[-1]) == str(CHILDREN-1)
|
assert label_of(tree.root.children[-1]) == str(CHILDREN - 1)
|
||||||
assert [label_of(node) for node in tree.root.children] == [str(n) for n in range(CHILDREN)]
|
assert [label_of(node) for node in tree.root.children] == [
|
||||||
assert [label_of(node) for node in tree.root.children[:2]] == [str(n) for n in range(2)]
|
str(n) for n in range(CHILDREN)
|
||||||
|
]
|
||||||
|
assert [label_of(node) for node in tree.root.children[:2]] == [
|
||||||
|
str(n) for n in range(2)
|
||||||
|
]
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
tree.root.children[0] = tree.root.children[1]
|
tree.root.children[0] = tree.root.children[1]
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from textual.widgets import Tree, TreeNode
|
from textual.widgets import Tree, TreeNode
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
|
|
||||||
def test_tree_node_label() -> None:
|
def test_tree_node_label() -> None:
|
||||||
"""It should be possible to modify a TreeNode's label."""
|
"""It should be possible to modify a TreeNode's label."""
|
||||||
node = TreeNode(Tree[None]("Xenomorph Lifecycle"), None, 0, "Facehugger")
|
node = TreeNode(Tree[None]("Xenomorph Lifecycle"), None, 0, "Facehugger")
|
||||||
@@ -8,6 +9,7 @@ def test_tree_node_label() -> None:
|
|||||||
node.label = "Chestbuster"
|
node.label = "Chestbuster"
|
||||||
assert node.label == Text("Chestbuster")
|
assert node.label == Text("Chestbuster")
|
||||||
|
|
||||||
|
|
||||||
def test_tree_node_label_via_tree() -> None:
|
def test_tree_node_label_via_tree() -> None:
|
||||||
"""It should be possible to modify a TreeNode's label when created via a Tree."""
|
"""It should be possible to modify a TreeNode's label when created via a Tree."""
|
||||||
tree = Tree[None]("Xenomorph Lifecycle")
|
tree = Tree[None]("Xenomorph Lifecycle")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from textual.widgets import TreeNode, Tree
|
from textual.widgets import TreeNode, Tree
|
||||||
|
|
||||||
|
|
||||||
def test_tree_node_parent() -> None:
|
def test_tree_node_parent() -> None:
|
||||||
"""It should be possible to access a TreeNode's parent."""
|
"""It should be possible to access a TreeNode's parent."""
|
||||||
tree = Tree[None]("Anakin")
|
tree = Tree[None]("Anakin")
|
||||||
|
|||||||
Reference in New Issue
Block a user