├── .coveragerc
├── .gitignore
├── .gitmodules
├── .travis.yml
├── LICENSE
├── README.md
├── plasma
├── __init__.py
├── debug.py
├── enum.py
├── layout.py
└── node.py
├── setup.cfg
├── setup.py
├── tests
├── __init__.py
├── conftest.py
├── test_debug.py
├── test_layout.py
└── test_node.py
├── tools
└── make_readme.py
└── tox.ini
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | branch = True
3 | source = plasma
4 |
5 | [paths]
6 | source =
7 | plasma
8 | .tox/*/lib/python*/site-packages/plasma
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__/
2 | *.py[cod]
3 | *$py.class
4 | *.egg-info/
5 | .coverage
6 | .coverage.*
7 | .cache/
8 | build/
9 | dist/
10 | TODO
11 | .pytest_cache/
12 | .tox/
13 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "lib/qtile"]
2 | path = lib/qtile
3 | url = https://github.com/qtile/qtile
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "3.7"
4 | - "3.8"
5 | - "3.9"
6 | addons:
7 | apt:
8 | packages:
9 | - xserver-xephyr
10 | jobs:
11 | include:
12 | - python: 3.9
13 | env: TOXENV=lint
14 | install:
15 | - pip install tox-travis codecov
16 | script:
17 | - tox
18 | after_success:
19 | - codecov
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 numirias
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Plasma
2 |
3 | [](https://travis-ci.org/numirias/qtile-plasma)
4 | [](https://codecov.io/gh/numirias/qtile-plasma)
5 | [](https://pypi.python.org/pypi/qtile-plasma)
6 | [](https://pypi.python.org/pypi/qtile-plasma)
7 |
8 | Plasma is a flexible, tree-based layout for [Qtile](https://github.com/qtile/qtile/).
9 |
10 | If you're looking for a well-tested and maintained alternative to Qtile's default layouts, give it a try.
11 |
12 | ## About
13 |
14 | Plasma works on a tree structure. Each node represents a container with child containers aligned either horizontally or vertically (similar to [i3](https://i3wm.org/)). Each window is attached to a leaf, taking either a proportional or a specific custom amount of space in its parent container. Windows can be resized, rearranged and integrated into other containers, enabling lots of different setups.
15 |
16 | ## Demo
17 |
18 | Here is a quick demo showing some of the main features (adding modes, moving, integrating and resizing):
19 |
20 | 
21 |
22 | ## Installation
23 |
24 | Install the package. You can [get it from PyPI](https://pypi.python.org/pypi/qtile-plasma/):
25 |
26 | ```
27 | pip install --upgrade qtile-plasma
28 | ```
29 |
30 | Then, add the layout to your config (`~/.config/qtile/config.py`):
31 |
32 | ```python
33 | from plasma import Plasma
34 | ...
35 | layouts = [
36 | Plasma(
37 | border_normal='#333333',
38 | border_focus='#00e891',
39 | border_normal_fixed='#006863',
40 | border_focus_fixed='#00e8dc',
41 | border_width=1,
42 | border_width_single=0,
43 | margin=0
44 | ),
45 | ...
46 | ]
47 | ```
48 |
49 | Add some key bindings, too. I am using these:
50 |
51 | ```python
52 | from libqtile.command import lazy
53 | from libqtile.config import EzKey
54 | ...
55 | keymap = {
56 | 'M-h': lazy.layout.left(),
57 | 'M-j': lazy.layout.down(),
58 | 'M-k': lazy.layout.up(),
59 | 'M-l': lazy.layout.right(),
60 | 'M-S-h': lazy.layout.move_left(),
61 | 'M-S-j': lazy.layout.move_down(),
62 | 'M-S-k': lazy.layout.move_up(),
63 | 'M-S-l': lazy.layout.move_right(),
64 | 'M-A-h': lazy.layout.integrate_left(),
65 | 'M-A-j': lazy.layout.integrate_down(),
66 | 'M-A-k': lazy.layout.integrate_up(),
67 | 'M-A-l': lazy.layout.integrate_right(),
68 | 'M-d': lazy.layout.mode_horizontal(),
69 | 'M-v': lazy.layout.mode_vertical(),
70 | 'M-S-d': lazy.layout.mode_horizontal_split(),
71 | 'M-S-v': lazy.layout.mode_vertical_split(),
72 | 'M-a': lazy.layout.grow_width(30),
73 | 'M-x': lazy.layout.grow_width(-30),
74 | 'M-S-a': lazy.layout.grow_height(30),
75 | 'M-S-x': lazy.layout.grow_height(-30),
76 | 'M-C-5': lazy.layout.size(500),
77 | 'M-C-8': lazy.layout.size(800),
78 | 'M-n': lazy.layout.reset_size(),
79 | }
80 | keys = [EzKey(k, v) for k, v in keymap.items()]
81 | ```
82 |
83 | Done!
84 |
85 | ## Commands
86 |
87 | The layout exposes the following commands:
88 |
89 |
90 |
91 |
92 | next() |
93 | Focus next window. |
94 |
95 |
96 | previous() |
97 | Focus previous window. |
98 |
99 |
100 | recent() |
101 | Focus most recently focused window.
102 | (Toggles between the two latest active windows.) |
103 |
104 |
105 | left() |
106 | Focus window to the left. |
107 |
108 |
109 | right() |
110 | Focus window to the right. |
111 |
112 |
113 | up() |
114 | Focus window above. |
115 |
116 |
117 | down() |
118 | Focus window below. |
119 |
120 |
121 | move_left() |
122 | Move current window left. |
123 |
124 |
125 | move_right() |
126 | Move current window right. |
127 |
128 |
129 | move_up() |
130 | Move current window up. |
131 |
132 |
133 | move_down() |
134 | Move current window down. |
135 |
136 |
137 | integrate_left() |
138 | Integrate current window left. |
139 |
140 |
141 | integrate_right() |
142 | Integrate current window right. |
143 |
144 |
145 | integrate_up() |
146 | Integrate current window up. |
147 |
148 |
149 | integrate_down() |
150 | Integrate current window down. |
151 |
152 |
153 | mode_horizontal() |
154 | Next window will be added horizontally. |
155 |
156 |
157 | mode_vertical() |
158 | Next window will be added vertically. |
159 |
160 |
161 | mode_horizontal_split() |
162 | Next window will be added horizontally, splitting space of current
163 | window. |
164 |
165 |
166 | mode_vertical_split() |
167 | Next window will be added vertically, splitting space of current
168 | window. |
169 |
170 |
171 | size(x) |
172 | Change size of current window.
173 | (It's recommended to use width() /height() instead.) |
174 |
175 |
176 | width(x) |
177 | Set width of current window. |
178 |
179 |
180 | height(x) |
181 | Set height of current window. |
182 |
183 |
184 | reset_size() |
185 | Reset size of current window to automatic (relative) sizing. |
186 |
187 |
188 | grow(x) |
189 | Grow size of current window.
190 | (It's recommended to use grow_width() /grow_height() instead.) |
191 |
192 |
193 | grow_width(x) |
194 | Grow width of current window. |
195 |
196 |
197 | grow_height(x) |
198 | Grow height of current window. |
199 |
200 |
201 |
202 |
203 | ## Contributing
204 |
205 | If you have found a bug or want to suggest a feature, please [file an issue](https://github.com/numirias/qtile-plasma/issues/new).
206 |
207 |
208 | To work on Plasma locally, you need to clone submodules too, since the layout integration tests use some of Qtile's test fixtures:
209 |
210 | ```
211 | git clone --recursive https://github.com/numirias/qtile-plasma/
212 | ```
213 |
214 | Also make sure you meet the [hacking requirements of Qtile](http://docs.qtile.org/en/latest/manual/hacking.html). In particular, have `xserver-xephyr` installed. Then run:
215 |
216 | ```
217 | make init
218 | ```
219 |
220 | If that fails, run the `init` instructions from the [Makefile](https://github.com/numirias/qtile-plasma/blob/master/Makefile) one by one.
221 |
222 | All new changes need to be fully test-covered and pass the linting:
223 |
224 | ```
225 | make lint
226 | make test
227 | ```
228 |
229 | If you made changes to the layout API, also re-build this README's [commands](#commands) section:
230 |
231 | ```
232 | make readme
233 | ```
234 |
--------------------------------------------------------------------------------
/plasma/__init__.py:
--------------------------------------------------------------------------------
1 | from .layout import Plasma
2 |
--------------------------------------------------------------------------------
/plasma/debug.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 |
3 |
4 | class Canvas:
5 |
6 | horizontal_line = '\u2500'
7 | vertical_line = '\u2502'
8 | tl_corner = '\u250c'
9 | tr_corner = '\u2510'
10 | bl_corner = '\u2514'
11 | br_corner = '\u2518'
12 |
13 | def __init__(self, width, height):
14 | self.width = width
15 | self.height = height
16 | self.canvas = defaultdict(lambda: defaultdict(lambda: '#'))
17 |
18 | def add_box(self, x, y, width, height, name='*'):
19 | width = width-1
20 | height = height-1
21 | label = str(name)[:width-1]
22 | for i in range(x, width+x):
23 | self.canvas[i][y] = self.horizontal_line
24 | self.canvas[i][y+height] = self.horizontal_line
25 | for i in range(y, height+y):
26 | self.canvas[x][i] = self.vertical_line
27 | self.canvas[x+width][i] = self.vertical_line
28 | for i in range(x+1, width+x):
29 | for j in range(y+1, y+height):
30 | self.canvas[i][j] = '.'
31 | for i, char in enumerate(label):
32 | self.canvas[x+1+i][y+1] = char
33 | self.canvas[x][y] = self.tl_corner
34 | self.canvas[x+width][y] = self.tr_corner
35 | self.canvas[x][y+height] = self.bl_corner
36 | self.canvas[x+width][y+height] = self.br_corner
37 |
38 | def view(self):
39 | res = ''
40 | for y in range(self.height):
41 | for x in range(self.width):
42 | res += self.canvas[x][y]
43 | res += '\n'
44 | return res
45 |
46 | def tree(node, level=0):
47 | res = '{indent}{name} {orient} {repr_} {pos} {size} {parent}\n'.format(
48 | indent=level*4*' ',
49 | name='%s' % (node.payload or '*'),
50 | orient='H' if node.horizontal else 'V',
51 | repr_='%s' % repr(node),
52 | pos='%g*%g@%g:%g' % (node.width, node.height, node.x, node.y),
53 | size='size: %s%s' % (node.size, ' (auto)' if node.flexible else ''),
54 | parent='p: %s' % node.parent,
55 | )
56 | for child in node:
57 | res += tree(child, level+1)
58 | return res
59 |
60 | def draw(root):
61 | canvas = Canvas(root.width, root.height)
62 | def add(node):
63 | if node.is_leaf:
64 | canvas.add_box(
65 | *node.pixel_perfect,
66 | node.payload
67 | )
68 | for child in node:
69 | add(child)
70 | add(root)
71 | return canvas.view()
72 |
73 | def info(node):
74 | print(tree(node))
75 | print(draw(node))
76 |
--------------------------------------------------------------------------------
/plasma/enum.py:
--------------------------------------------------------------------------------
1 | # flake8: noqa
2 | # pylint: skip-file
3 | # This is an exact copy of lib/enum.py from Python 3.6 to make Flag and auto
4 | # available in Python 3.5 without extra dependencies.
5 | # https://github.com/python/cpython/blob/3.6/Lib/enum.py
6 |
7 | import sys
8 | from types import MappingProxyType, DynamicClassAttribute
9 | from functools import reduce
10 | from operator import or_ as _or_
11 |
12 | # try _collections first to reduce startup cost
13 | try:
14 | from _collections import OrderedDict
15 | except ImportError:
16 | from collections import OrderedDict
17 |
18 |
19 | __all__ = [
20 | 'EnumMeta',
21 | 'Enum', 'IntEnum', 'Flag', 'IntFlag',
22 | 'auto', 'unique',
23 | ]
24 |
25 |
26 | def _is_descriptor(obj):
27 | """Returns True if obj is a descriptor, False otherwise."""
28 | return (
29 | hasattr(obj, '__get__') or
30 | hasattr(obj, '__set__') or
31 | hasattr(obj, '__delete__'))
32 |
33 |
34 | def _is_dunder(name):
35 | """Returns True if a __dunder__ name, False otherwise."""
36 | return (name[:2] == name[-2:] == '__' and
37 | name[2:3] != '_' and
38 | name[-3:-2] != '_' and
39 | len(name) > 4)
40 |
41 |
42 | def _is_sunder(name):
43 | """Returns True if a _sunder_ name, False otherwise."""
44 | return (name[0] == name[-1] == '_' and
45 | name[1:2] != '_' and
46 | name[-2:-1] != '_' and
47 | len(name) > 2)
48 |
49 | def _make_class_unpicklable(cls):
50 | """Make the given class un-picklable."""
51 | def _break_on_call_reduce(self, proto):
52 | raise TypeError('%r cannot be pickled' % self)
53 | cls.__reduce_ex__ = _break_on_call_reduce
54 | cls.__module__ = ''
55 |
56 | _auto_null = object()
57 | class auto:
58 | """
59 | Instances are replaced with an appropriate value in Enum class suites.
60 | """
61 | value = _auto_null
62 |
63 |
64 | class _EnumDict(dict):
65 | """Track enum member order and ensure member names are not reused.
66 |
67 | EnumMeta will use the names found in self._member_names as the
68 | enumeration member names.
69 |
70 | """
71 | def __init__(self):
72 | super().__init__()
73 | self._member_names = []
74 | self._last_values = []
75 |
76 | def __setitem__(self, key, value):
77 | """Changes anything not dundered or not a descriptor.
78 |
79 | If an enum member name is used twice, an error is raised; duplicate
80 | values are not checked for.
81 |
82 | Single underscore (sunder) names are reserved.
83 |
84 | """
85 | if _is_sunder(key):
86 | if key not in (
87 | '_order_', '_create_pseudo_member_',
88 | '_generate_next_value_', '_missing_',
89 | ):
90 | raise ValueError('_names_ are reserved for future Enum use')
91 | if key == '_generate_next_value_':
92 | setattr(self, '_generate_next_value', value)
93 | elif _is_dunder(key):
94 | if key == '__order__':
95 | key = '_order_'
96 | elif key in self._member_names:
97 | # descriptor overwriting an enum?
98 | raise TypeError('Attempted to reuse key: %r' % key)
99 | elif not _is_descriptor(value):
100 | if key in self:
101 | # enum overwriting a descriptor?
102 | raise TypeError('%r already defined as: %r' % (key, self[key]))
103 | if isinstance(value, auto):
104 | if value.value == _auto_null:
105 | value.value = self._generate_next_value(key, 1, len(self._member_names), self._last_values[:])
106 | value = value.value
107 | self._member_names.append(key)
108 | self._last_values.append(value)
109 | super().__setitem__(key, value)
110 |
111 |
112 | # Dummy value for Enum as EnumMeta explicitly checks for it, but of course
113 | # until EnumMeta finishes running the first time the Enum class doesn't exist.
114 | # This is also why there are checks in EnumMeta like `if Enum is not None`
115 | Enum = None
116 |
117 |
118 | class EnumMeta(type):
119 | """Metaclass for Enum"""
120 | @classmethod
121 | def __prepare__(metacls, cls, bases):
122 | # create the namespace dict
123 | enum_dict = _EnumDict()
124 | # inherit previous flags and _generate_next_value_ function
125 | member_type, first_enum = metacls._get_mixins_(bases)
126 | if first_enum is not None:
127 | enum_dict['_generate_next_value_'] = getattr(first_enum, '_generate_next_value_', None)
128 | return enum_dict
129 |
130 | def __new__(metacls, cls, bases, classdict):
131 | # an Enum class is final once enumeration items have been defined; it
132 | # cannot be mixed with other types (int, float, etc.) if it has an
133 | # inherited __new__ unless a new __new__ is defined (or the resulting
134 | # class will fail).
135 | member_type, first_enum = metacls._get_mixins_(bases)
136 | __new__, save_new, use_args = metacls._find_new_(classdict, member_type,
137 | first_enum)
138 |
139 | # save enum items into separate mapping so they don't get baked into
140 | # the new class
141 | enum_members = {k: classdict[k] for k in classdict._member_names}
142 | for name in classdict._member_names:
143 | del classdict[name]
144 |
145 | # adjust the sunders
146 | _order_ = classdict.pop('_order_', None)
147 |
148 | # check for illegal enum names (any others?)
149 | invalid_names = set(enum_members) & {'mro', }
150 | if invalid_names:
151 | raise ValueError('Invalid enum member name: {0}'.format(
152 | ','.join(invalid_names)))
153 |
154 | # create a default docstring if one has not been provided
155 | if '__doc__' not in classdict:
156 | classdict['__doc__'] = 'An enumeration.'
157 |
158 | # create our new Enum type
159 | enum_class = super().__new__(metacls, cls, bases, classdict)
160 | enum_class._member_names_ = [] # names in definition order
161 | enum_class._member_map_ = OrderedDict() # name->value map
162 | enum_class._member_type_ = member_type
163 |
164 | # save attributes from super classes so we know if we can take
165 | # the shortcut of storing members in the class dict
166 | base_attributes = {a for b in enum_class.mro() for a in b.__dict__}
167 |
168 | # Reverse value->name map for hashable values.
169 | enum_class._value2member_map_ = {}
170 |
171 | # If a custom type is mixed into the Enum, and it does not know how
172 | # to pickle itself, pickle.dumps will succeed but pickle.loads will
173 | # fail. Rather than have the error show up later and possibly far
174 | # from the source, sabotage the pickle protocol for this class so
175 | # that pickle.dumps also fails.
176 | #
177 | # However, if the new class implements its own __reduce_ex__, do not
178 | # sabotage -- it's on them to make sure it works correctly. We use
179 | # __reduce_ex__ instead of any of the others as it is preferred by
180 | # pickle over __reduce__, and it handles all pickle protocols.
181 | if '__reduce_ex__' not in classdict:
182 | if member_type is not object:
183 | methods = ('__getnewargs_ex__', '__getnewargs__',
184 | '__reduce_ex__', '__reduce__')
185 | if not any(m in member_type.__dict__ for m in methods):
186 | _make_class_unpicklable(enum_class)
187 |
188 | # instantiate them, checking for duplicates as we go
189 | # we instantiate first instead of checking for duplicates first in case
190 | # a custom __new__ is doing something funky with the values -- such as
191 | # auto-numbering ;)
192 | for member_name in classdict._member_names:
193 | value = enum_members[member_name]
194 | if not isinstance(value, tuple):
195 | args = (value, )
196 | else:
197 | args = value
198 | if member_type is tuple: # special case for tuple enums
199 | args = (args, ) # wrap it one more time
200 | if not use_args:
201 | enum_member = __new__(enum_class)
202 | if not hasattr(enum_member, '_value_'):
203 | enum_member._value_ = value
204 | else:
205 | enum_member = __new__(enum_class, *args)
206 | if not hasattr(enum_member, '_value_'):
207 | if member_type is object:
208 | enum_member._value_ = value
209 | else:
210 | enum_member._value_ = member_type(*args)
211 | value = enum_member._value_
212 | enum_member._name_ = member_name
213 | enum_member.__objclass__ = enum_class
214 | enum_member.__init__(*args)
215 | # If another member with the same value was already defined, the
216 | # new member becomes an alias to the existing one.
217 | for name, canonical_member in enum_class._member_map_.items():
218 | if canonical_member._value_ == enum_member._value_:
219 | enum_member = canonical_member
220 | break
221 | else:
222 | # Aliases don't appear in member names (only in __members__).
223 | enum_class._member_names_.append(member_name)
224 | # performance boost for any member that would not shadow
225 | # a DynamicClassAttribute
226 | if member_name not in base_attributes:
227 | setattr(enum_class, member_name, enum_member)
228 | # now add to _member_map_
229 | enum_class._member_map_[member_name] = enum_member
230 | try:
231 | # This may fail if value is not hashable. We can't add the value
232 | # to the map, and by-value lookups for this value will be
233 | # linear.
234 | enum_class._value2member_map_[value] = enum_member
235 | except TypeError:
236 | pass
237 |
238 | # double check that repr and friends are not the mixin's or various
239 | # things break (such as pickle)
240 | for name in ('__repr__', '__str__', '__format__', '__reduce_ex__'):
241 | class_method = getattr(enum_class, name)
242 | obj_method = getattr(member_type, name, None)
243 | enum_method = getattr(first_enum, name, None)
244 | if obj_method is not None and obj_method is class_method:
245 | setattr(enum_class, name, enum_method)
246 |
247 | # replace any other __new__ with our own (as long as Enum is not None,
248 | # anyway) -- again, this is to support pickle
249 | if Enum is not None:
250 | # if the user defined their own __new__, save it before it gets
251 | # clobbered in case they subclass later
252 | if save_new:
253 | enum_class.__new_member__ = __new__
254 | enum_class.__new__ = Enum.__new__
255 |
256 | # py3 support for definition order (helps keep py2/py3 code in sync)
257 | if _order_ is not None:
258 | if isinstance(_order_, str):
259 | _order_ = _order_.replace(',', ' ').split()
260 | if _order_ != enum_class._member_names_:
261 | raise TypeError('member order does not match _order_')
262 |
263 | return enum_class
264 |
265 | def __bool__(self):
266 | """
267 | classes/types should always be True.
268 | """
269 | return True
270 |
271 | def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, start=1):
272 | """Either returns an existing member, or creates a new enum class.
273 |
274 | This method is used both when an enum class is given a value to match
275 | to an enumeration member (i.e. Color(3)) and for the functional API
276 | (i.e. Color = Enum('Color', names='RED GREEN BLUE')).
277 |
278 | When used for the functional API:
279 |
280 | `value` will be the name of the new class.
281 |
282 | `names` should be either a string of white-space/comma delimited names
283 | (values will start at `start`), or an iterator/mapping of name, value pairs.
284 |
285 | `module` should be set to the module this class is being created in;
286 | if it is not set, an attempt to find that module will be made, but if
287 | it fails the class will not be picklable.
288 |
289 | `qualname` should be set to the actual location this class can be found
290 | at in its module; by default it is set to the global scope. If this is
291 | not correct, unpickling will fail in some circumstances.
292 |
293 | `type`, if set, will be mixed in as the first base class.
294 |
295 | """
296 | if names is None: # simple value lookup
297 | return cls.__new__(cls, value)
298 | # otherwise, functional API: we're creating a new Enum type
299 | return cls._create_(value, names, module=module, qualname=qualname, type=type, start=start)
300 |
301 | def __contains__(cls, member):
302 | return isinstance(member, cls) and member._name_ in cls._member_map_
303 |
304 | def __delattr__(cls, attr):
305 | # nicer error message when someone tries to delete an attribute
306 | # (see issue19025).
307 | if attr in cls._member_map_:
308 | raise AttributeError(
309 | "%s: cannot delete Enum member." % cls.__name__)
310 | super().__delattr__(attr)
311 |
312 | def __dir__(self):
313 | return (['__class__', '__doc__', '__members__', '__module__'] +
314 | self._member_names_)
315 |
316 | def __getattr__(cls, name):
317 | """Return the enum member matching `name`
318 |
319 | We use __getattr__ instead of descriptors or inserting into the enum
320 | class' __dict__ in order to support `name` and `value` being both
321 | properties for enum members (which live in the class' __dict__) and
322 | enum members themselves.
323 |
324 | """
325 | if _is_dunder(name):
326 | raise AttributeError(name)
327 | try:
328 | return cls._member_map_[name]
329 | except KeyError:
330 | raise AttributeError(name) from None
331 |
332 | def __getitem__(cls, name):
333 | return cls._member_map_[name]
334 |
335 | def __iter__(cls):
336 | return (cls._member_map_[name] for name in cls._member_names_)
337 |
338 | def __len__(cls):
339 | return len(cls._member_names_)
340 |
341 | @property
342 | def __members__(cls):
343 | """Returns a mapping of member name->value.
344 |
345 | This mapping lists all enum members, including aliases. Note that this
346 | is a read-only view of the internal mapping.
347 |
348 | """
349 | return MappingProxyType(cls._member_map_)
350 |
351 | def __repr__(cls):
352 | return "" % cls.__name__
353 |
354 | def __reversed__(cls):
355 | return (cls._member_map_[name] for name in reversed(cls._member_names_))
356 |
357 | def __setattr__(cls, name, value):
358 | """Block attempts to reassign Enum members.
359 |
360 | A simple assignment to the class namespace only changes one of the
361 | several possible ways to get an Enum member from the Enum class,
362 | resulting in an inconsistent Enumeration.
363 |
364 | """
365 | member_map = cls.__dict__.get('_member_map_', {})
366 | if name in member_map:
367 | raise AttributeError('Cannot reassign members.')
368 | super().__setattr__(name, value)
369 |
370 | def _create_(cls, class_name, names=None, *, module=None, qualname=None, type=None, start=1):
371 | """Convenience method to create a new Enum class.
372 |
373 | `names` can be:
374 |
375 | * A string containing member names, separated either with spaces or
376 | commas. Values are incremented by 1 from `start`.
377 | * An iterable of member names. Values are incremented by 1 from `start`.
378 | * An iterable of (member name, value) pairs.
379 | * A mapping of member name -> value pairs.
380 |
381 | """
382 | metacls = cls.__class__
383 | bases = (cls, ) if type is None else (type, cls)
384 | _, first_enum = cls._get_mixins_(bases)
385 | classdict = metacls.__prepare__(class_name, bases)
386 |
387 | # special processing needed for names?
388 | if isinstance(names, str):
389 | names = names.replace(',', ' ').split()
390 | if isinstance(names, (tuple, list)) and names and isinstance(names[0], str):
391 | original_names, names = names, []
392 | last_values = []
393 | for count, name in enumerate(original_names):
394 | value = first_enum._generate_next_value_(name, start, count, last_values[:])
395 | last_values.append(value)
396 | names.append((name, value))
397 |
398 | # Here, names is either an iterable of (name, value) or a mapping.
399 | for item in names:
400 | if isinstance(item, str):
401 | member_name, member_value = item, names[item]
402 | else:
403 | member_name, member_value = item
404 | classdict[member_name] = member_value
405 | enum_class = metacls.__new__(metacls, class_name, bases, classdict)
406 |
407 | # TODO: replace the frame hack if a blessed way to know the calling
408 | # module is ever developed
409 | if module is None:
410 | try:
411 | module = sys._getframe(2).f_globals['__name__']
412 | except (AttributeError, ValueError) as exc:
413 | pass
414 | if module is None:
415 | _make_class_unpicklable(enum_class)
416 | else:
417 | enum_class.__module__ = module
418 | if qualname is not None:
419 | enum_class.__qualname__ = qualname
420 |
421 | return enum_class
422 |
423 | @staticmethod
424 | def _get_mixins_(bases):
425 | """Returns the type for creating enum members, and the first inherited
426 | enum class.
427 |
428 | bases: the tuple of bases that was given to __new__
429 |
430 | """
431 | if not bases:
432 | return object, Enum
433 |
434 | # double check that we are not subclassing a class with existing
435 | # enumeration members; while we're at it, see if any other data
436 | # type has been mixed in so we can use the correct __new__
437 | member_type = first_enum = None
438 | for base in bases:
439 | if (base is not Enum and
440 | issubclass(base, Enum) and
441 | base._member_names_):
442 | raise TypeError("Cannot extend enumerations")
443 | # base is now the last base in bases
444 | if not issubclass(base, Enum):
445 | raise TypeError("new enumerations must be created as "
446 | "`ClassName([mixin_type,] enum_type)`")
447 |
448 | # get correct mix-in type (either mix-in type of Enum subclass, or
449 | # first base if last base is Enum)
450 | if not issubclass(bases[0], Enum):
451 | member_type = bases[0] # first data type
452 | first_enum = bases[-1] # enum type
453 | else:
454 | for base in bases[0].__mro__:
455 | # most common: (IntEnum, int, Enum, object)
456 | # possible: (, ,
457 | # , ,
458 | # )
459 | if issubclass(base, Enum):
460 | if first_enum is None:
461 | first_enum = base
462 | else:
463 | if member_type is None:
464 | member_type = base
465 |
466 | return member_type, first_enum
467 |
468 | @staticmethod
469 | def _find_new_(classdict, member_type, first_enum):
470 | """Returns the __new__ to be used for creating the enum members.
471 |
472 | classdict: the class dictionary given to __new__
473 | member_type: the data type whose __new__ will be used by default
474 | first_enum: enumeration to check for an overriding __new__
475 |
476 | """
477 | # now find the correct __new__, checking to see of one was defined
478 | # by the user; also check earlier enum classes in case a __new__ was
479 | # saved as __new_member__
480 | __new__ = classdict.get('__new__', None)
481 |
482 | # should __new__ be saved as __new_member__ later?
483 | save_new = __new__ is not None
484 |
485 | if __new__ is None:
486 | # check all possibles for __new_member__ before falling back to
487 | # __new__
488 | for method in ('__new_member__', '__new__'):
489 | for possible in (member_type, first_enum):
490 | target = getattr(possible, method, None)
491 | if target not in {
492 | None,
493 | None.__new__,
494 | object.__new__,
495 | Enum.__new__,
496 | }:
497 | __new__ = target
498 | break
499 | if __new__ is not None:
500 | break
501 | else:
502 | __new__ = object.__new__
503 |
504 | # if a non-object.__new__ is used then whatever value/tuple was
505 | # assigned to the enum member name will be passed to __new__ and to the
506 | # new enum member's __init__
507 | if __new__ is object.__new__:
508 | use_args = False
509 | else:
510 | use_args = True
511 |
512 | return __new__, save_new, use_args
513 |
514 |
515 | class Enum(metaclass=EnumMeta):
516 | """Generic enumeration.
517 |
518 | Derive from this class to define new enumerations.
519 |
520 | """
521 | def __new__(cls, value):
522 | # all enum instances are actually created during class construction
523 | # without calling this method; this method is called by the metaclass'
524 | # __call__ (i.e. Color(3) ), and by pickle
525 | if type(value) is cls:
526 | # For lookups like Color(Color.RED)
527 | return value
528 | # by-value search for a matching enum member
529 | # see if it's in the reverse mapping (for hashable values)
530 | try:
531 | if value in cls._value2member_map_:
532 | return cls._value2member_map_[value]
533 | except TypeError:
534 | # not there, now do long search -- O(n) behavior
535 | for member in cls._member_map_.values():
536 | if member._value_ == value:
537 | return member
538 | # still not found -- try _missing_ hook
539 | return cls._missing_(value)
540 |
541 | def _generate_next_value_(name, start, count, last_values):
542 | for last_value in reversed(last_values):
543 | try:
544 | return last_value + 1
545 | except TypeError:
546 | pass
547 | else:
548 | return start
549 |
550 | @classmethod
551 | def _missing_(cls, value):
552 | raise ValueError("%r is not a valid %s" % (value, cls.__name__))
553 |
554 | def __repr__(self):
555 | return "<%s.%s: %r>" % (
556 | self.__class__.__name__, self._name_, self._value_)
557 |
558 | def __str__(self):
559 | return "%s.%s" % (self.__class__.__name__, self._name_)
560 |
561 | def __dir__(self):
562 | added_behavior = [
563 | m
564 | for cls in self.__class__.mro()
565 | for m in cls.__dict__
566 | if m[0] != '_' and m not in self._member_map_
567 | ]
568 | return (['__class__', '__doc__', '__module__'] + added_behavior)
569 |
570 | def __format__(self, format_spec):
571 | # mixed-in Enums should use the mixed-in type's __format__, otherwise
572 | # we can get strange results with the Enum name showing up instead of
573 | # the value
574 |
575 | # pure Enum branch
576 | if self._member_type_ is object:
577 | cls = str
578 | val = str(self)
579 | # mix-in branch
580 | else:
581 | cls = self._member_type_
582 | val = self._value_
583 | return cls.__format__(val, format_spec)
584 |
585 | def __hash__(self):
586 | return hash(self._name_)
587 |
588 | def __reduce_ex__(self, proto):
589 | return self.__class__, (self._value_, )
590 |
591 | # DynamicClassAttribute is used to provide access to the `name` and
592 | # `value` properties of enum members while keeping some measure of
593 | # protection from modification, while still allowing for an enumeration
594 | # to have members named `name` and `value`. This works because enumeration
595 | # members are not set directly on the enum class -- __getattr__ is
596 | # used to look them up.
597 |
598 | @DynamicClassAttribute
599 | def name(self):
600 | """The name of the Enum member."""
601 | return self._name_
602 |
603 | @DynamicClassAttribute
604 | def value(self):
605 | """The value of the Enum member."""
606 | return self._value_
607 |
608 | @classmethod
609 | def _convert(cls, name, module, filter, source=None):
610 | """
611 | Create a new Enum subclass that replaces a collection of global constants
612 | """
613 | # convert all constants from source (or module) that pass filter() to
614 | # a new Enum called name, and export the enum and its members back to
615 | # module;
616 | # also, replace the __reduce_ex__ method so unpickling works in
617 | # previous Python versions
618 | module_globals = vars(sys.modules[module])
619 | if source:
620 | source = vars(source)
621 | else:
622 | source = module_globals
623 | # We use an OrderedDict of sorted source keys so that the
624 | # _value2member_map is populated in the same order every time
625 | # for a consistent reverse mapping of number to name when there
626 | # are multiple names for the same number rather than varying
627 | # between runs due to hash randomization of the module dictionary.
628 | members = [
629 | (name, source[name])
630 | for name in source.keys()
631 | if filter(name)]
632 | try:
633 | # sort by value
634 | members.sort(key=lambda t: (t[1], t[0]))
635 | except TypeError:
636 | # unless some values aren't comparable, in which case sort by name
637 | members.sort(key=lambda t: t[0])
638 | cls = cls(name, members, module=module)
639 | cls.__reduce_ex__ = _reduce_ex_by_name
640 | module_globals.update(cls.__members__)
641 | module_globals[name] = cls
642 | return cls
643 |
644 |
645 | class IntEnum(int, Enum):
646 | """Enum where members are also (and must be) ints"""
647 |
648 |
649 | def _reduce_ex_by_name(self, proto):
650 | return self.name
651 |
652 | class Flag(Enum):
653 | """Support for flags"""
654 |
655 | def _generate_next_value_(name, start, count, last_values):
656 | """
657 | Generate the next value when not given.
658 |
659 | name: the name of the member
660 | start: the initital start value or None
661 | count: the number of existing members
662 | last_value: the last value assigned or None
663 | """
664 | if not count:
665 | return start if start is not None else 1
666 | for last_value in reversed(last_values):
667 | try:
668 | high_bit = _high_bit(last_value)
669 | break
670 | except Exception:
671 | raise TypeError('Invalid Flag value: %r' % last_value) from None
672 | return 2 ** (high_bit+1)
673 |
674 | @classmethod
675 | def _missing_(cls, value):
676 | original_value = value
677 | if value < 0:
678 | value = ~value
679 | possible_member = cls._create_pseudo_member_(value)
680 | if original_value < 0:
681 | possible_member = ~possible_member
682 | return possible_member
683 |
684 | @classmethod
685 | def _create_pseudo_member_(cls, value):
686 | """
687 | Create a composite member iff value contains only members.
688 | """
689 | pseudo_member = cls._value2member_map_.get(value, None)
690 | if pseudo_member is None:
691 | # verify all bits are accounted for
692 | _, extra_flags = _decompose(cls, value)
693 | if extra_flags:
694 | raise ValueError("%r is not a valid %s" % (value, cls.__name__))
695 | # construct a singleton enum pseudo-member
696 | pseudo_member = object.__new__(cls)
697 | pseudo_member._name_ = None
698 | pseudo_member._value_ = value
699 | # use setdefault in case another thread already created a composite
700 | # with this value
701 | pseudo_member = cls._value2member_map_.setdefault(value, pseudo_member)
702 | return pseudo_member
703 |
704 | def __contains__(self, other):
705 | if not isinstance(other, self.__class__):
706 | return NotImplemented
707 | return other._value_ & self._value_ == other._value_
708 |
709 | def __repr__(self):
710 | cls = self.__class__
711 | if self._name_ is not None:
712 | return '<%s.%s: %r>' % (cls.__name__, self._name_, self._value_)
713 | members, uncovered = _decompose(cls, self._value_)
714 | return '<%s.%s: %r>' % (
715 | cls.__name__,
716 | '|'.join([str(m._name_ or m._value_) for m in members]),
717 | self._value_,
718 | )
719 |
720 | def __str__(self):
721 | cls = self.__class__
722 | if self._name_ is not None:
723 | return '%s.%s' % (cls.__name__, self._name_)
724 | members, uncovered = _decompose(cls, self._value_)
725 | if len(members) == 1 and members[0]._name_ is None:
726 | return '%s.%r' % (cls.__name__, members[0]._value_)
727 | else:
728 | return '%s.%s' % (
729 | cls.__name__,
730 | '|'.join([str(m._name_ or m._value_) for m in members]),
731 | )
732 |
733 | def __bool__(self):
734 | return bool(self._value_)
735 |
736 | def __or__(self, other):
737 | if not isinstance(other, self.__class__):
738 | return NotImplemented
739 | return self.__class__(self._value_ | other._value_)
740 |
741 | def __and__(self, other):
742 | if not isinstance(other, self.__class__):
743 | return NotImplemented
744 | return self.__class__(self._value_ & other._value_)
745 |
746 | def __xor__(self, other):
747 | if not isinstance(other, self.__class__):
748 | return NotImplemented
749 | return self.__class__(self._value_ ^ other._value_)
750 |
751 | def __invert__(self):
752 | members, uncovered = _decompose(self.__class__, self._value_)
753 | inverted_members = [
754 | m for m in self.__class__
755 | if m not in members and not m._value_ & self._value_
756 | ]
757 | inverted = reduce(_or_, inverted_members, self.__class__(0))
758 | return self.__class__(inverted)
759 |
760 |
761 | class IntFlag(int, Flag):
762 | """Support for integer-based Flags"""
763 |
764 | @classmethod
765 | def _missing_(cls, value):
766 | if not isinstance(value, int):
767 | raise ValueError("%r is not a valid %s" % (value, cls.__name__))
768 | new_member = cls._create_pseudo_member_(value)
769 | return new_member
770 |
771 | @classmethod
772 | def _create_pseudo_member_(cls, value):
773 | pseudo_member = cls._value2member_map_.get(value, None)
774 | if pseudo_member is None:
775 | need_to_create = [value]
776 | # get unaccounted for bits
777 | _, extra_flags = _decompose(cls, value)
778 | # timer = 10
779 | while extra_flags:
780 | # timer -= 1
781 | bit = _high_bit(extra_flags)
782 | flag_value = 2 ** bit
783 | if (flag_value not in cls._value2member_map_ and
784 | flag_value not in need_to_create
785 | ):
786 | need_to_create.append(flag_value)
787 | if extra_flags == -flag_value:
788 | extra_flags = 0
789 | else:
790 | extra_flags ^= flag_value
791 | for value in reversed(need_to_create):
792 | # construct singleton pseudo-members
793 | pseudo_member = int.__new__(cls, value)
794 | pseudo_member._name_ = None
795 | pseudo_member._value_ = value
796 | # use setdefault in case another thread already created a composite
797 | # with this value
798 | pseudo_member = cls._value2member_map_.setdefault(value, pseudo_member)
799 | return pseudo_member
800 |
801 | def __or__(self, other):
802 | if not isinstance(other, (self.__class__, int)):
803 | return NotImplemented
804 | result = self.__class__(self._value_ | self.__class__(other)._value_)
805 | return result
806 |
807 | def __and__(self, other):
808 | if not isinstance(other, (self.__class__, int)):
809 | return NotImplemented
810 | return self.__class__(self._value_ & self.__class__(other)._value_)
811 |
812 | def __xor__(self, other):
813 | if not isinstance(other, (self.__class__, int)):
814 | return NotImplemented
815 | return self.__class__(self._value_ ^ self.__class__(other)._value_)
816 |
817 | __ror__ = __or__
818 | __rand__ = __and__
819 | __rxor__ = __xor__
820 |
821 | def __invert__(self):
822 | result = self.__class__(~self._value_)
823 | return result
824 |
825 |
826 | def _high_bit(value):
827 | """returns index of highest bit, or -1 if value is zero or negative"""
828 | return value.bit_length() - 1
829 |
830 | def unique(enumeration):
831 | """Class decorator for enumerations ensuring unique member values."""
832 | duplicates = []
833 | for name, member in enumeration.__members__.items():
834 | if name != member.name:
835 | duplicates.append((name, member.name))
836 | if duplicates:
837 | alias_details = ', '.join(
838 | ["%s -> %s" % (alias, name) for (alias, name) in duplicates])
839 | raise ValueError('duplicate values found in %r: %s' %
840 | (enumeration, alias_details))
841 | return enumeration
842 |
843 | def _decompose(flag, value):
844 | """Extract all members from the value."""
845 | # _decompose is only called if the value is not named
846 | not_covered = value
847 | negative = value < 0
848 | # issue29167: wrap accesses to _value2member_map_ in a list to avoid race
849 | # conditions between iterating over it and having more psuedo-
850 | # members added to it
851 | if negative:
852 | # only check for named flags
853 | flags_to_check = [
854 | (m, v)
855 | for v, m in list(flag._value2member_map_.items())
856 | if m.name is not None
857 | ]
858 | else:
859 | # check for named flags and powers-of-two flags
860 | flags_to_check = [
861 | (m, v)
862 | for v, m in list(flag._value2member_map_.items())
863 | if m.name is not None or _power_of_two(v)
864 | ]
865 | members = []
866 | for member, member_value in flags_to_check:
867 | if member_value and member_value & value == member_value:
868 | members.append(member)
869 | not_covered &= ~member_value
870 | if not members and value in flag._value2member_map_:
871 | members.append(flag._value2member_map_[value])
872 | members.sort(key=lambda m: m._value_, reverse=True)
873 | if len(members) > 1 and members[0].value == value:
874 | # we have the breakdown, don't need the value member itself
875 | members.pop(0)
876 | return members, not_covered
877 |
878 | def _power_of_two(value):
879 | if value < 1:
880 | return False
881 | return value == 2 ** _high_bit(value)
882 |
--------------------------------------------------------------------------------
/plasma/layout.py:
--------------------------------------------------------------------------------
1 | import copy
2 |
3 | from xcffib.xproto import StackMode
4 | from libqtile.layout.base import Layout
5 |
6 | from .node import Node, AddMode, NotRestorableError
7 |
8 |
9 | class Plasma(Layout):
10 | """A flexible tree-based layout.
11 |
12 | Each tree node represents a container whose children are aligned either
13 | horizontally or vertically. Each window is attached to a leaf of the tree
14 | and takes either a calculated relative amount or a custom absolute amount
15 | of space in its parent container. Windows can be resized, rearranged and
16 | integrated into other containers.
17 | """
18 | defaults = [
19 | ('name', 'Plasma', 'Layout name'),
20 | ('border_normal', '#333333', 'Unfocused window border color'),
21 | ('border_focus', '#00e891', 'Focused window border color'),
22 | ('border_normal_fixed', '#333333',
23 | 'Unfocused fixed-size window border color'),
24 | ('border_focus_fixed', '#00e8dc',
25 | 'Focused fixed-size window border color'),
26 | ('border_width', 1, 'Border width'),
27 | ('border_width_single', 0, 'Border width for single window'),
28 | ('margin', 0, 'Layout margin'),
29 | ]
30 | # If windows are added before configure() was called, the screen size is
31 | # still unknown, so we need to set some arbitrary initial root dimensions
32 | default_dimensions = (0, 0, 1000, 1000)
33 |
34 | def __init__(self, **config):
35 | Layout.__init__(self, **config)
36 | self.add_defaults(Plasma.defaults)
37 | self.root = Node(None, *self.default_dimensions)
38 | self.focused = None
39 | self.add_mode = None
40 |
41 | @staticmethod
42 | def convert_names(tree):
43 | return [Plasma.convert_names(n) if isinstance(n, list) else
44 | n.payload.name for n in tree]
45 |
46 | @property
47 | def focused_node(self):
48 | return self.root.find_payload(self.focused)
49 |
50 | def info(self):
51 | info = super().info()
52 | info['tree'] = self.convert_names(self.root.tree)
53 | return info
54 |
55 | def clone(self, group):
56 | clone = copy.copy(self)
57 | clone.group = group
58 | clone.root = Node(None, *self.default_dimensions)
59 | clone.focused = None
60 | clone.add_mode = None
61 | return clone
62 |
63 | def add(self, client):
64 | node = self.root if self.focused_node is None else self.focused_node
65 | new = Node(client)
66 | try:
67 | self.root.restore(new)
68 | except NotRestorableError:
69 | node.add_node(new, self.add_mode)
70 | self.add_mode = None
71 |
72 | def remove(self, client):
73 | self.root.find_payload(client).remove()
74 |
75 | def configure(self, client, screen_rect):
76 | self.root.x = screen_rect.x
77 | self.root.y = screen_rect.y
78 | self.root.width = screen_rect.width
79 | self.root.height = screen_rect.height
80 | node = self.root.find_payload(client)
81 | border_width = self.border_width_single if self.root.tree == [node] \
82 | else self.border_width
83 | border_color = getattr(self, 'border_' +
84 | ('focus' if client.has_focus else 'normal') +
85 | ('' if node.flexible else '_fixed'))
86 | x, y, width, height = node.pixel_perfect
87 | client.place(
88 | x,
89 | y,
90 | width-2*border_width,
91 | height-2*border_width,
92 | border_width,
93 | border_color,
94 | margin=self.margin,
95 | )
96 | # Always keep tiles below floating windows
97 | client.window.configure(stackmode=StackMode.Below)
98 | client.unhide()
99 |
100 | def focus(self, client):
101 | self.focused = client
102 | self.root.find_payload(client).access()
103 |
104 | def focus_first(self):
105 | return self.root.first_leaf.payload
106 |
107 | def focus_last(self):
108 | return self.root.last_leaf.payload
109 |
110 | def focus_next(self, win):
111 | next_leaf = self.root.find_payload(win).next_leaf
112 | return None if next_leaf is self.root.first_leaf else next_leaf.payload
113 |
114 | def focus_previous(self, win):
115 | prev_leaf = self.root.find_payload(win).prev_leaf
116 | return None if prev_leaf is self.root.last_leaf else prev_leaf.payload
117 |
118 | def focus_node(self, node):
119 | if node is None:
120 | return
121 | self.group.focus(node.payload)
122 |
123 | def refocus(self):
124 | self.group.focus(self.focused)
125 |
126 | def cmd_next(self):
127 | """Focus next window."""
128 | self.focus_node(self.focused_node.next_leaf)
129 |
130 | def cmd_previous(self):
131 | """Focus previous window."""
132 | self.focus_node(self.focused_node.prev_leaf)
133 |
134 | def cmd_recent(self):
135 | """Focus most recently focused window.
136 |
137 | (Toggles between the two latest active windows.)
138 | """
139 | nodes = [n for n in self.root.all_leafs if n is not self.focused_node]
140 | most_recent = max(nodes, key=lambda n: n.last_accessed)
141 | self.focus_node(most_recent)
142 |
143 | def cmd_left(self):
144 | """Focus window to the left."""
145 | self.focus_node(self.focused_node.close_left)
146 |
147 | def cmd_right(self):
148 | """Focus window to the right."""
149 | self.focus_node(self.focused_node.close_right)
150 |
151 | def cmd_up(self):
152 | """Focus window above."""
153 | self.focus_node(self.focused_node.close_up)
154 |
155 | def cmd_down(self):
156 | """Focus window below."""
157 | self.focus_node(self.focused_node.close_down)
158 |
159 | def cmd_move_left(self):
160 | """Move current window left."""
161 | self.focused_node.move_left()
162 | self.refocus()
163 |
164 | def cmd_move_right(self):
165 | """Move current window right."""
166 | self.focused_node.move_right()
167 | self.refocus()
168 |
169 | def cmd_move_up(self):
170 | """Move current window up."""
171 | self.focused_node.move_up()
172 | self.refocus()
173 |
174 | def cmd_move_down(self):
175 | """Move current window down."""
176 | self.focused_node.move_down()
177 | self.refocus()
178 |
179 | def cmd_integrate_left(self):
180 | """Integrate current window left."""
181 | self.focused_node.integrate_left()
182 | self.refocus()
183 |
184 | def cmd_integrate_right(self):
185 | """Integrate current window right."""
186 | self.focused_node.integrate_right()
187 | self.refocus()
188 |
189 | def cmd_integrate_up(self):
190 | """Integrate current window up."""
191 | self.focused_node.integrate_up()
192 | self.refocus()
193 |
194 | def cmd_integrate_down(self):
195 | """Integrate current window down."""
196 | self.focused_node.integrate_down()
197 | self.refocus()
198 |
199 | def cmd_mode_horizontal(self):
200 | """Next window will be added horizontally."""
201 | self.add_mode = AddMode.HORIZONTAL
202 |
203 | def cmd_mode_vertical(self):
204 | """Next window will be added vertically."""
205 | self.add_mode = AddMode.VERTICAL
206 |
207 | def cmd_mode_horizontal_split(self):
208 | """Next window will be added horizontally, splitting space of current
209 | window.
210 | """
211 | self.add_mode = AddMode.HORIZONTAL | AddMode.SPLIT
212 |
213 | def cmd_mode_vertical_split(self):
214 | """Next window will be added vertically, splitting space of current
215 | window.
216 | """
217 | self.add_mode = AddMode.VERTICAL | AddMode.SPLIT
218 |
219 | def cmd_size(self, x):
220 | """Change size of current window.
221 |
222 | (It's recommended to use `width()`/`height()` instead.)
223 | """
224 | self.focused_node.size = x
225 | self.refocus()
226 |
227 | def cmd_width(self, x):
228 | """Set width of current window."""
229 | self.focused_node.width = x
230 | self.refocus()
231 |
232 | def cmd_height(self, x):
233 | """Set height of current window."""
234 | self.focused_node.height = x
235 | self.refocus()
236 |
237 | def cmd_reset_size(self):
238 | """Reset size of current window to automatic (relative) sizing."""
239 | self.focused_node.reset_size()
240 | self.refocus()
241 |
242 | def cmd_grow(self, x):
243 | """Grow size of current window.
244 |
245 | (It's recommended to use `grow_width()`/`grow_height()` instead.)
246 | """
247 | self.focused_node.size += x
248 | self.refocus()
249 |
250 | def cmd_grow_width(self, x):
251 | """Grow width of current window."""
252 | self.focused_node.width += x
253 | self.refocus()
254 |
255 | def cmd_grow_height(self, x):
256 | """Grow height of current window."""
257 | self.focused_node.height += x
258 | self.refocus()
259 |
--------------------------------------------------------------------------------
/plasma/node.py:
--------------------------------------------------------------------------------
1 | from collections import namedtuple
2 | import time
3 | from math import isclose
4 | import sys
5 |
6 | if sys.version_info >= (3, 6):
7 | from enum import Enum, Flag, auto
8 | else:
9 | # Python 3.5 backport
10 | from .enum import Enum, Flag, auto
11 |
12 |
13 | Point = namedtuple('Point', 'x y')
14 | Dimensions = namedtuple('Dimensions', 'x y width height')
15 |
16 | class Orient(Flag):
17 | HORIZONTAL = 0
18 | VERTICAL = 1
19 |
20 | HORIZONTAL, VERTICAL = Orient
21 |
22 | class Direction(Enum):
23 | UP = auto()
24 | DOWN = auto()
25 | LEFT = auto()
26 | RIGHT = auto()
27 |
28 | @property
29 | def orient(self):
30 | return HORIZONTAL if self in [self.LEFT, self.RIGHT] else VERTICAL
31 |
32 | @property
33 | def offset(self):
34 | return 1 if self in [self.RIGHT, self.DOWN] else -1
35 |
36 | UP, DOWN, LEFT, RIGHT = Direction
37 |
38 | class AddMode(Flag):
39 | HORIZONTAL = 0
40 | VERTICAL = 1
41 | SPLIT = auto()
42 |
43 | @property
44 | def orient(self):
45 | return VERTICAL if self & self.VERTICAL else HORIZONTAL
46 |
47 | border_check = {
48 | UP: lambda a, b: isclose(a.y, b.y_end),
49 | DOWN: lambda a, b: isclose(a.y_end, b.y),
50 | LEFT: lambda a, b: isclose(a.x, b.x_end),
51 | RIGHT: lambda a, b: isclose(a.x_end, b.x),
52 | }
53 |
54 | class NotRestorableError(Exception):
55 | pass
56 |
57 | class Node:
58 | """A tree node.
59 |
60 | Each node represents a container that can hold a payload and child nodes.
61 | """
62 | min_size_default = 100
63 | root_orient = HORIZONTAL
64 |
65 | def __init__(self, payload=None, x=None, y=None, width=None, height=None):
66 | self.payload = payload
67 | self._x = x
68 | self._y = y
69 | self._width = width
70 | self._height = height
71 | self._size = None
72 | self.children = []
73 | self.last_accessed = 0
74 | self.parent = None
75 | self.restorables = {}
76 |
77 | def __repr__(self):
78 | info = self.payload or ''
79 | if self:
80 | info += ' +%d' % len(self)
81 | return '' % (info, id(self))
82 |
83 | def __contains__(self, node):
84 | if node is self:
85 | return True
86 | for child in self:
87 | if node in child:
88 | return True
89 | return False
90 |
91 | def __iter__(self):
92 | yield from self.children
93 |
94 | def __getitem__(self, key):
95 | return self.children[key]
96 |
97 | def __setitem__(self, key, value):
98 | self.children[key] = value
99 |
100 | def __len__(self):
101 | return len(self.children)
102 |
103 | @property
104 | def root(self):
105 | try:
106 | return self.parent.root
107 | except AttributeError:
108 | return self
109 |
110 | @property
111 | def is_root(self):
112 | return self.parent is None
113 |
114 | @property
115 | def is_leaf(self):
116 | return not self
117 |
118 | @property
119 | def index(self):
120 | return self.parent.children.index(self)
121 |
122 | @property
123 | def tree(self):
124 | return [c.tree if c else c for c in self]
125 |
126 | @property
127 | def siblings(self):
128 | return [c for c in self.parent if c is not self]
129 |
130 | @property
131 | def first_leaf(self):
132 | if self.is_leaf:
133 | return self
134 | return self[0].first_leaf
135 |
136 | @property
137 | def last_leaf(self):
138 | if self.is_leaf:
139 | return self
140 | return self[-1].last_leaf
141 |
142 | @property
143 | def recent_leaf(self):
144 | if self.is_leaf:
145 | return self
146 | return max(self, key=lambda n: n.last_accessed).recent_leaf
147 |
148 | @property
149 | def prev_leaf(self):
150 | if self.is_root:
151 | return self.last_leaf
152 | idx = self.index - 1
153 | if idx < 0:
154 | return self.parent.prev_leaf
155 | return self.parent[idx].last_leaf
156 |
157 | @property
158 | def next_leaf(self):
159 | if self.is_root:
160 | return self.first_leaf
161 | idx = self.index + 1
162 | if idx >= len(self.parent):
163 | return self.parent.next_leaf
164 | return self.parent[idx].first_leaf
165 |
166 | @property
167 | def all_leafs(self):
168 | if self.is_leaf:
169 | yield self
170 | for child in self:
171 | yield from child.all_leafs
172 |
173 | @property
174 | def orient(self):
175 | if self.is_root:
176 | return self.root_orient
177 | return ~self.parent.orient
178 |
179 | @property
180 | def horizontal(self):
181 | return self.orient is HORIZONTAL
182 |
183 | @property
184 | def vertical(self):
185 | return self.orient is VERTICAL
186 |
187 | @property
188 | def x(self):
189 | if self.is_root:
190 | return self._x
191 | if self.horizontal:
192 | return self.parent.x
193 | return self.parent.x + self.size_offset
194 |
195 | @x.setter
196 | def x(self, val):
197 | if not self.is_root:
198 | return
199 | self._x = val
200 |
201 | @property
202 | def y(self):
203 | if self.is_root:
204 | return self._y
205 | if self.vertical:
206 | return self.parent.y
207 | return self.parent.y + self.size_offset
208 |
209 | @y.setter
210 | def y(self, val):
211 | if not self.is_root:
212 | return
213 | self._y = val
214 |
215 | @property
216 | def pos(self):
217 | return Point(self.x, self.y)
218 |
219 | @property
220 | def width(self):
221 | if self.is_root:
222 | return self._width
223 | if self.horizontal:
224 | return self.parent.width
225 | return self.size
226 |
227 | @width.setter
228 | def width(self, val):
229 | if self.is_root:
230 | self._width = val
231 | elif self.horizontal:
232 | self.parent.size = val
233 | else:
234 | self.size = val
235 |
236 | @property
237 | def height(self):
238 | if self.is_root:
239 | return self._height
240 | if self.vertical:
241 | return self.parent.height
242 | return self.size
243 |
244 | @height.setter
245 | def height(self, val):
246 | if self.is_root:
247 | self._height = val
248 | elif self.vertical:
249 | self.parent.size = val
250 | else:
251 | self.size = val
252 |
253 | @property
254 | def x_end(self):
255 | return self.x + self.width
256 |
257 | @property
258 | def y_end(self):
259 | return self.y + self.height
260 |
261 | @property
262 | def x_center(self):
263 | return self.x + self.width / 2
264 |
265 | @property
266 | def y_center(self):
267 | return self.y + self.height / 2
268 |
269 | @property
270 | def center(self):
271 | return Point(self.x_center, self.y_center)
272 |
273 | @property
274 | def top_left(self):
275 | return Point(self.x, self.y)
276 |
277 | @property
278 | def top_right(self):
279 | return Point(self.x + self.width, self.y)
280 |
281 | @property
282 | def bottom_left(self):
283 | return Point(self.x, self.y + self.height)
284 |
285 | @property
286 | def bottom_right(self):
287 | return Point(self.x + self.width, self.y + self.height)
288 |
289 | @property
290 | def pixel_perfect(self):
291 | """Return pixel-perfect int dimensions (x, y, width, height) which
292 | compensate for gaps in the layout grid caused by plain int conversion.
293 | """
294 | x, y, width, height = self.x, self.y, self.width, self.height
295 | threshold = 0.99999
296 | if (x - int(x)) + (width - int(width)) > threshold:
297 | width += 1
298 | if (y - int(y)) + (height - int(height)) > threshold:
299 | height += 1
300 | return Dimensions(*map(int, (x, y, width, height)))
301 |
302 | @property
303 | def capacity(self):
304 | return self.width if self.horizontal else self.height
305 |
306 | @property
307 | def size(self):
308 | """Return amount of space taken in parent container."""
309 | if self.is_root:
310 | return None
311 | if self.fixed:
312 | return self._size
313 | if self.flexible:
314 | # Distribute space evenly among flexible nodes
315 | taken = sum(n.size for n in self.siblings if not n.flexible)
316 | flexibles = [n for n in self.parent if n.flexible]
317 | return (self.parent.capacity - taken) / len(flexibles)
318 | return max(sum(gc.min_size for gc in c) for c in self)
319 |
320 | @size.setter
321 | def size(self, val):
322 | if self.is_root or not self.siblings:
323 | return
324 | if val is None:
325 | self.reset_size()
326 | return
327 | occupied = sum(s.min_size_bound for s in self.siblings)
328 | val = max(min(val, self.parent.capacity - occupied),
329 | self.min_size_bound)
330 | self.force_size(val)
331 |
332 | def force_size(self, val):
333 | """Set size without considering available space."""
334 | Node.fit_into(self.siblings, self.parent.capacity - val)
335 | if val == 0:
336 | return
337 | if self:
338 | Node.fit_into([self], val)
339 | self._size = val
340 |
341 | @property
342 | def size_offset(self):
343 | return sum(c.size for c in self.parent[:self.index])
344 |
345 | @staticmethod
346 | def fit_into(nodes, space):
347 | """Resize nodes to fit them into the available space."""
348 | if not nodes:
349 | return
350 | occupied = sum(n.min_size for n in nodes)
351 | if space >= occupied and any(n.flexible for n in nodes):
352 | # If any flexible node exists, it will occupy the space
353 | # automatically, not requiring any action.
354 | return
355 | nodes_left = nodes[:]
356 | space_left = space
357 | if space < occupied:
358 | for node in nodes:
359 | if node.min_size_bound != node.min_size:
360 | continue
361 | # Substract nodes that are already at their minimal possible
362 | # size because they can't be shrinked any further.
363 | space_left -= node.min_size
364 | nodes_left.remove(node)
365 | if not nodes_left:
366 | return
367 | factor = space_left / sum(n.size for n in nodes_left)
368 | for node in nodes_left:
369 | new_size = node.size * factor
370 | if node.fixed:
371 | node._size = new_size # pylint: disable=protected-access
372 | for child in node:
373 | Node.fit_into(child, new_size)
374 |
375 | @property
376 | def fixed(self):
377 | """A node is fixed if it has a specified size."""
378 | return self._size is not None
379 |
380 | @property
381 | def min_size(self):
382 | if self.fixed:
383 | return self._size
384 | if self.is_leaf:
385 | return self.min_size_default
386 | size = max(sum(gc.min_size for gc in c) for c in self)
387 | return max(size, self.min_size_default)
388 |
389 | @property
390 | def min_size_bound(self):
391 | if self.is_leaf:
392 | return self.min_size_default
393 | return max(sum(gc.min_size_bound for gc in c) or
394 | self.min_size_default for c in self)
395 |
396 | def reset_size(self):
397 | self._size = None
398 |
399 | @property
400 | def flexible(self):
401 | """A node is flexible if its size isn't (explicitly or implictly)
402 | determined.
403 | """
404 | if self.fixed:
405 | return False
406 | return all((any(gc.flexible for gc in c) or c.is_leaf) for c in self)
407 |
408 | def access(self):
409 | self.last_accessed = time.time()
410 | try:
411 | self.parent.access()
412 | except AttributeError:
413 | pass
414 |
415 | def neighbor(self, direction):
416 | """Return adjacent leaf node in specified direction."""
417 | if self.is_root:
418 | return None
419 | if direction.orient is self.parent.orient:
420 | target_idx = self.index + direction.offset
421 | if 0 <= target_idx < len(self.parent):
422 | return self.parent[target_idx].recent_leaf
423 | if self.parent.is_root:
424 | return None
425 | return self.parent.parent.neighbor(direction)
426 | return self.parent.neighbor(direction)
427 |
428 | @property
429 | def up(self):
430 | return self.neighbor(UP)
431 |
432 | @property
433 | def down(self):
434 | return self.neighbor(DOWN)
435 |
436 | @property
437 | def left(self):
438 | return self.neighbor(LEFT)
439 |
440 | @property
441 | def right(self):
442 | return self.neighbor(RIGHT)
443 |
444 | def common_border(self, node, direction):
445 | """Return whether a common border with given node in specified
446 | direction exists.
447 | """
448 | if not border_check[direction](self, node):
449 | return False
450 | if direction in [UP, DOWN]:
451 | detached = node.x >= self.x_end or node.x_end <= self.x
452 | else:
453 | detached = node.y >= self.y_end or node.y_end <= self.y
454 | return not detached
455 |
456 | def close_neighbor(self, direction):
457 | """Return visually adjacent leaf node in specified direction."""
458 | nodes = [n for n in self.root.all_leafs if
459 | self.common_border(n, direction)]
460 | if not nodes:
461 | return None
462 | most_recent = max(nodes, key=lambda n: n.last_accessed)
463 | if most_recent.last_accessed > 0:
464 | return most_recent
465 | if direction in [UP, DOWN]:
466 | match = lambda n: n.x <= self.x_center <= n.x_end
467 | else:
468 | match = lambda n: n.y <= self.y_center <= n.y_end
469 | return next(n for n in nodes if match(n))
470 |
471 | @property
472 | def close_up(self):
473 | return self.close_neighbor(UP)
474 |
475 | @property
476 | def close_down(self):
477 | return self.close_neighbor(DOWN)
478 |
479 | @property
480 | def close_left(self):
481 | return self.close_neighbor(LEFT)
482 |
483 | @property
484 | def close_right(self):
485 | return self.close_neighbor(RIGHT)
486 |
487 | def add_child(self, node, idx=None):
488 | if idx is None:
489 | idx = len(self)
490 | self.children.insert(idx, node)
491 | node.parent = self
492 | if len(self) == 1:
493 | return
494 | total = self.capacity
495 | Node.fit_into(node.siblings, total - (total / len(self)))
496 |
497 | def add_child_after(self, new, old):
498 | self.add_child(new, idx=old.index+1)
499 |
500 | def remove_child(self, node):
501 | node._save_restore_state() # pylint: disable=W0212
502 | node.force_size(0)
503 | self.children.remove(node)
504 | if len(self) == 1:
505 | child = self[0]
506 | if self.is_root:
507 | # A single child doesn't need a fixed size
508 | child.reset_size()
509 | else:
510 | # Collapse tree with a single child
511 | self.parent.replace_child(self, child)
512 | Node.fit_into(child, self.capacity)
513 |
514 | def remove(self):
515 | self.parent.remove_child(self)
516 |
517 | def replace_child(self, old, new):
518 | self[old.index] = new
519 | new.parent = self
520 | new._size = old._size # pylint: disable=protected-access
521 |
522 | def flip_with(self, node, reverse=False):
523 | """Join with node in a new, orthogonal container."""
524 | container = Node()
525 | self.parent.replace_child(self, container)
526 | self.reset_size()
527 | for child in [node, self] if reverse else [self, node]:
528 | container.add_child(child)
529 |
530 | def add_node(self, node, mode=None):
531 | """Add node according to the mode.
532 |
533 | This can result in adding it as a child, joining with it in a new
534 | flipped sub-container, or splitting the space with it.
535 | """
536 | if self.is_root:
537 | self.add_child(node)
538 | elif mode is None:
539 | self.parent.add_child_after(node, self)
540 | elif mode.orient is self.parent.orient:
541 | if mode & AddMode.SPLIT:
542 | node._size = 0 # pylint: disable=protected-access
543 | self.parent.add_child_after(node, self)
544 | self._size = node._size = self.size / 2
545 | else:
546 | self.parent.add_child_after(node, self)
547 | else:
548 | self.flip_with(node)
549 |
550 | def restore(self, node):
551 | """Restore node.
552 |
553 | Try to add the node in a place where a node with the same payload
554 | has previously been.
555 | """
556 | restorables = self.root.restorables
557 | try:
558 | parent, idx, sizes, fixed, flip = restorables[node.payload]
559 | except KeyError:
560 | raise NotRestorableError() # pylint: disable=raise-missing-from
561 | if parent not in self.root:
562 | # Don't try to restore if parent is not part of the tree anymore
563 | raise NotRestorableError()
564 | node.reset_size()
565 | if flip:
566 | old_parent_size = parent.size
567 | parent.flip_with(node, reverse=(idx == 0))
568 | node.size, parent.size = sizes
569 | Node.fit_into(parent, old_parent_size)
570 | else:
571 | parent.add_child(node, idx=idx)
572 | node.size = sizes[0]
573 | if len(sizes) == 2:
574 | node.siblings[0].size = sizes[1]
575 | if not fixed:
576 | node.reset_size()
577 | del restorables[node.payload]
578 |
579 | def _save_restore_state(self):
580 | parent = self.parent
581 | sizes = (self.size,)
582 | flip = False
583 | if len(self.siblings) == 1:
584 | # If there is only one node left in the container, we need to save
585 | # its size too because the size will be lost.
586 | sizes += (self.siblings[0]._size,) # pylint: disable=W0212
587 | if not self.parent.is_root:
588 | flip = True
589 | parent = self.siblings[0]
590 | self.root.restorables[self.payload] = (parent, self.index, sizes,
591 | self.fixed, flip)
592 |
593 | def move(self, direction):
594 | """Move this node in `direction`. Return whether node was moved."""
595 | if self.is_root:
596 | return False
597 | if direction.orient is self.parent.orient:
598 | old_idx = self.index
599 | new_idx = old_idx + direction.offset
600 | if 0 <= new_idx < len(self.parent):
601 | p = self.parent
602 | p[old_idx], p[new_idx] = p[new_idx], p[old_idx]
603 | return True
604 | new_sibling = self.parent.parent
605 | else:
606 | new_sibling = self.parent
607 | try:
608 | new_parent = new_sibling.parent
609 | idx = new_sibling.index
610 | except AttributeError:
611 | return False
612 | self.reset_size()
613 | self.parent.remove_child(self)
614 | new_parent.add_child(self, idx + (1 if direction.offset == 1 else 0))
615 | return True
616 |
617 | def move_up(self):
618 | return self.move(UP)
619 |
620 | def move_down(self):
621 | return self.move(DOWN)
622 |
623 | def move_right(self):
624 | return self.move(RIGHT)
625 |
626 | def move_left(self):
627 | return self.move(LEFT)
628 |
629 | def _move_and_integrate(self, direction):
630 | old_parent = self.parent
631 | self.move(direction)
632 | if self.parent is not old_parent:
633 | self.integrate(direction)
634 |
635 | def integrate(self, direction):
636 | if direction.orient != self.parent.orient:
637 | self._move_and_integrate(direction)
638 | return
639 | target_idx = self.index + direction.offset
640 | if target_idx < 0 or target_idx >= len(self.parent):
641 | self._move_and_integrate(direction)
642 | return
643 | self.reset_size()
644 | target = self.parent[target_idx]
645 | self.parent.remove_child(self)
646 | if target.is_leaf:
647 | target.flip_with(self)
648 | else:
649 | target.add_child(self)
650 |
651 | def integrate_up(self):
652 | self.integrate(UP)
653 |
654 | def integrate_down(self):
655 | self.integrate(DOWN)
656 |
657 | def integrate_left(self):
658 | self.integrate(LEFT)
659 |
660 | def integrate_right(self):
661 | self.integrate(RIGHT)
662 |
663 | def find_payload(self, payload):
664 | if self.payload is payload:
665 | return self
666 | for child in self:
667 | needle = child.find_payload(payload)
668 | if needle is not None:
669 | return needle
670 | return None
671 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [pylint]
2 | disable =
3 | invalid-name,
4 | missing-docstring,
5 | too-few-public-methods,
6 | too-many-public-methods,
7 | too-many-arguments,
8 | too-many-instance-attributes,
9 | unsupported-assignment-operation,
10 | unsubscriptable-object,
11 | not-an-iterable,
12 | fixme,
13 | no-name-in-module,
14 | attribute-defined-outside-init,
15 |
16 | [flake8]
17 | ignore = E226,E302,E305,E306,E731,F811,W504
18 | exclude = __init__.py,lib/
19 |
20 | [coverage:run]
21 | omit =
22 | plasma/enum.py
23 |
24 | [coverage:report]
25 | exclude_lines =
26 | from enum import
27 | from .enum import
28 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup(
4 | name='qtile-plasma',
5 | packages=['plasma'],
6 | version='1.5.6',
7 | description='A flexible, tree-based layout for Qtile',
8 | author='numirias',
9 | author_email='numirias@users.noreply.github.com',
10 | url='https://github.com/numirias/qtile-plasma',
11 | license='MIT',
12 | python_requires='>=3',
13 | install_requires=['xcffib>=0.5.0', 'qtile>=0.17'],
14 | classifiers=[
15 | 'Development Status :: 4 - Beta',
16 | 'License :: OSI Approved :: MIT License',
17 | 'Operating System :: Unix',
18 | 'Programming Language :: Python',
19 | 'Programming Language :: Python :: 3',
20 | 'Programming Language :: Python :: 3.7',
21 | 'Programming Language :: Python :: 3.8',
22 | 'Programming Language :: Python :: 3.9',
23 | 'Topic :: Desktop Environment :: Window Managers',
24 | ],
25 | )
26 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/numirias/qtile-plasma/4b57f313ed6948212582de05205a3ae4372ed812/tests/__init__.py
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | import sys
3 |
4 | from pytest import fixture
5 |
6 | from plasma.node import Node
7 |
8 | # We borrow Qtile's testing framework. That's not elegant but the best option.
9 | sys.path.insert(0, str(Path(__file__).parents[1] / 'lib')) # noqa: E402
10 | from qtile.test.conftest import pytest_addoption
11 |
12 |
13 |
14 | Node.min_size_default = 10
15 |
16 | @fixture
17 | def root():
18 | root = Node('root', 0, 0, 120, 50)
19 | return root
20 |
21 | @fixture
22 | def tiny_grid(root):
23 | a, b, c = Nodes('a b c')
24 | root.add_child(a)
25 | root.add_child(b)
26 | b.flip_with(c)
27 | return a, b, c
28 |
29 | @fixture
30 | def small_grid(root):
31 | a, b, c, d = Nodes('a b c d')
32 | root.add_child(a)
33 | root.add_child(b)
34 | b.flip_with(c)
35 | c.flip_with(d)
36 | return a, b, c, d
37 |
38 | @fixture
39 | def grid(root):
40 | a, b, c, d, e = Nodes('a b c d e')
41 | root.add_child(a)
42 | root.add_child(b)
43 | b.flip_with(c)
44 | c.flip_with(d)
45 | c.parent.add_child(e)
46 | return a, b, c, d, e
47 |
48 | @fixture
49 | def complex_grid(root):
50 | a, b, c, d, e, f, g = Nodes('a b c d e f g')
51 | root.add_child(a)
52 | root.add_child(b)
53 | b.flip_with(c)
54 | c.flip_with(d)
55 | c.parent.add_child(e)
56 | c.flip_with(f)
57 | f.flip_with(g)
58 | return a, b, c, d, e, f, g
59 |
60 | def Nodes(string):
61 | for x in string.split():
62 | yield Node(x)
63 |
--------------------------------------------------------------------------------
/tests/test_debug.py:
--------------------------------------------------------------------------------
1 | from plasma.debug import draw, tree, info
2 |
3 |
4 | class TestDebugging:
5 |
6 | def test_tree(self, root, grid):
7 | lines = tree(root).split('\n')
8 | assert lines[0].startswith('root')
9 | assert lines[1].strip().startswith('a')
10 | assert lines[2].strip().startswith('*')
11 | assert lines[3].strip().startswith('b')
12 |
13 | def test_draw(self, root, grid):
14 | a, *_ = grid
15 | root._width = 24
16 | root._height = 10
17 | a.payload = 'XXXXXXXXXXXX'
18 | data = draw(root)
19 | assert data == '''
20 | ┌──────────┐┌──────────┐
21 | │XXXXXXXXXX││b.........│
22 | │..........││..........│
23 | │..........││..........│
24 | │..........│└──────────┘
25 | │..........│┌──┐┌──┐┌──┐
26 | │..........││c.││d.││e.│
27 | │..........││..││..││..│
28 | │..........││..││..││..│
29 | └──────────┘└──┘└──┘└──┘
30 | '''.replace(' ', '')[1:]
31 |
32 | def test_info(self, root, grid, capsys):
33 | info(root)
34 | out, _ = capsys.readouterr()
35 | assert out == tree(root) + '\n' + draw(root) + '\n'
36 |
--------------------------------------------------------------------------------
/tests/test_layout.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from pytest import fixture, mark
3 | import sys
4 | import time
5 |
6 | from plasma import Plasma
7 | from plasma.node import Node
8 |
9 | # We borrow Qtile's testing framework. That's not elegant but the best option.
10 | sys.path.insert(0, str(Path(__file__).parents[1] / 'lib')) # noqa: E402
11 | from qtile.libqtile import config
12 | from qtile.libqtile.layout import Floating
13 | from qtile.test.conftest import manager as qtile, no_xinerama, xephyr, xvfb # noqa: F401
14 | from qtile.test.layouts.layout_utils import assert_focused
15 | from qtile.libqtile.confreader import Config as _Config
16 |
17 |
18 | @fixture
19 | def grid(qtile):
20 | qtile.test_window('a')
21 | qtile.test_window('b')
22 | qtile.c.layout.previous()
23 | qtile.c.layout.mode_vertical()
24 | qtile.test_window('c')
25 | qtile.c.layout.right()
26 | qtile.c.layout.mode_vertical()
27 | qtile.test_window('d')
28 |
29 | class Config(_Config):
30 |
31 | auto_fullscreen = True
32 | main = None
33 | groups = [
34 | config.Group('g0'),
35 | config.Group('g1'),
36 | config.Group('g2'),
37 | config.Group('g3')
38 | ]
39 | layouts = [Plasma()]
40 | floating_layout = Floating()
41 | keys = []
42 | mouse = []
43 | screens = []
44 | follow_mouse_focus = False
45 |
46 | def plasma_config(func):
47 | config = mark.parametrize('qtile', [Config], indirect=True)(func)
48 | return no_xinerama(config)
49 |
50 | def tree(qtile):
51 | return qtile.c.layout.info()['tree']
52 |
53 | class TestLayout:
54 |
55 | def test_init(self):
56 | layout = Plasma()
57 | assert isinstance(layout.root, Node)
58 |
59 | def test_focus(self, root):
60 | layout = Plasma()
61 | layout.root = root
62 | a, b, c, d = 'abcd'
63 | layout.add(a)
64 | layout.add(b)
65 | layout.add(c)
66 | layout.add(d)
67 | assert layout.focus_first() == 'a'
68 | assert layout.focus_last() == 'd'
69 | assert layout.focus_next('b') == 'c'
70 | assert layout.focus_previous('c') == 'b'
71 | layout.focus('c')
72 | assert layout.focused is c
73 |
74 | def test_access(self, root):
75 | layout = Plasma()
76 | layout.root = root
77 | layout.add('a')
78 | now = time.time()
79 | assert layout.root.find_payload('a').last_accessed < now
80 | layout.focus('a')
81 | assert layout.root.find_payload('a').last_accessed > now
82 |
83 | @plasma_config
84 | def test_info(self, qtile):
85 | qtile.test_window('a')
86 | qtile.test_window('b')
87 | assert qtile.c.layout.info()['tree'] == ['a', 'b']
88 |
89 | @plasma_config
90 | def test_windows(self, qtile):
91 | qtile.test_window('a')
92 | qtile.test_window('b')
93 | qtile.test_window('c')
94 | assert_focused(qtile, 'c')
95 | assert tree(qtile) == ['a', 'b', 'c']
96 |
97 | @plasma_config
98 | def test_split_directions(self, qtile):
99 | qtile.test_window('a')
100 | qtile.c.layout.mode_horizontal()
101 | qtile.test_window('b')
102 | qtile.c.layout.mode_vertical()
103 | qtile.test_window('c')
104 | assert tree(qtile) == ['a', ['b', 'c']]
105 |
106 | @plasma_config
107 | def test_directions(self, qtile, grid):
108 | assert_focused(qtile, 'd')
109 | qtile.c.layout.left()
110 | assert_focused(qtile, 'c')
111 | qtile.c.layout.up()
112 | assert_focused(qtile, 'a')
113 | qtile.c.layout.right()
114 | assert_focused(qtile, 'b')
115 | qtile.c.layout.down()
116 | assert_focused(qtile, 'd')
117 | qtile.c.layout.down()
118 | assert_focused(qtile, 'd')
119 | qtile.c.layout.previous()
120 | assert_focused(qtile, 'b')
121 | qtile.c.layout.next()
122 | assert_focused(qtile, 'd')
123 |
124 | @plasma_config
125 | def test_move(self, qtile, grid):
126 | assert tree(qtile) == [['a', 'c'], ['b', 'd']]
127 | qtile.c.layout.move_up()
128 | assert tree(qtile) == [['a', 'c'], ['d', 'b']]
129 | qtile.c.layout.move_down()
130 | assert tree(qtile) == [['a', 'c'], ['b', 'd']]
131 | qtile.c.layout.move_left()
132 | assert tree(qtile) == [['a', 'c'], 'd', 'b']
133 | qtile.c.layout.move_right()
134 | assert tree(qtile) == [['a', 'c'], 'b', 'd']
135 |
136 | @plasma_config
137 | def test_integrate(self, qtile, grid):
138 | qtile.c.layout.integrate_left()
139 | assert tree(qtile) == [['a', 'c', 'd'], 'b']
140 | qtile.c.layout.integrate_up()
141 | assert tree(qtile) == [['a', ['c', 'd']], 'b']
142 | qtile.c.layout.integrate_up()
143 | qtile.c.layout.integrate_down()
144 | assert tree(qtile) == [['a', ['c', 'd']], 'b']
145 | qtile.c.layout.integrate_right()
146 | assert tree(qtile) == [['a', 'c'], ['b', 'd']]
147 |
148 | @plasma_config
149 | def test_sizes(self, qtile):
150 | qtile.test_window('a')
151 | qtile.test_window('b')
152 | qtile.c.layout.mode_vertical()
153 | qtile.test_window('c')
154 | info = qtile.c.window.info()
155 | assert info['x'] == 400
156 | assert info['y'] == 300
157 | assert info['width'] == 400 - 2
158 | assert info['height'] == 300 - 2
159 | qtile.c.layout.grow_height(50)
160 | info = qtile.c.window.info()
161 | assert info['height'] == 300 - 2 + 50
162 | qtile.c.layout.grow_width(50)
163 | info = qtile.c.window.info()
164 | assert info['width'] == 400 - 2 + 50
165 | qtile.c.layout.reset_size()
166 | info = qtile.c.window.info()
167 | assert info['height'] == 300 - 2
168 | qtile.c.layout.height(300)
169 | info = qtile.c.window.info()
170 | assert info['height'] == 300 - 2
171 | qtile.c.layout.width(250)
172 | info = qtile.c.window.info()
173 | assert info['width'] == 250 - 2
174 | qtile.c.layout.size(200)
175 | info = qtile.c.window.info()
176 | assert info['height'] == 200 - 2
177 | qtile.c.layout.grow(10)
178 | info = qtile.c.window.info()
179 | assert info['height'] == 210 - 2
180 |
181 | @plasma_config
182 | def test_remove(self, qtile):
183 | a = qtile.test_window('a')
184 | b = qtile.test_window('b')
185 | assert tree(qtile) == ['a', 'b']
186 | qtile.kill_window(a)
187 | assert tree(qtile) == ['b']
188 | qtile.kill_window(b)
189 | assert tree(qtile) == []
190 |
191 | @plasma_config
192 | def test_split_mode(self, qtile):
193 | qtile.test_window('a')
194 | qtile.test_window('b')
195 | qtile.c.layout.mode_horizontal_split()
196 | qtile.test_window('c')
197 | assert qtile.c.window.info()['width'] == 200 - 2
198 | qtile.c.layout.mode_vertical()
199 | qtile.test_window('d')
200 | assert qtile.c.window.info()['height'] == 300 - 2
201 | qtile.test_window('e')
202 | assert qtile.c.window.info()['height'] == 200 - 2
203 | qtile.c.layout.mode_vertical_split()
204 | qtile.test_window('f')
205 | assert qtile.c.window.info()['height'] == 100 - 2
206 |
207 | @plasma_config
208 | def test_recent(self, qtile):
209 | qtile.test_window('a')
210 | qtile.test_window('b')
211 | qtile.test_window('c')
212 | assert_focused(qtile, 'c')
213 | qtile.c.layout.recent()
214 | assert_focused(qtile, 'b')
215 | qtile.c.layout.recent()
216 | assert_focused(qtile, 'c')
217 | qtile.c.layout.next()
218 | assert_focused(qtile, 'a')
219 | qtile.c.layout.recent()
220 | assert_focused(qtile, 'c')
221 |
222 | def test_bug_10(self):
223 | """Adding nodes when the correct root dimensions are still unknown
224 | should not raise an error.
225 | """
226 | layout = Plasma()
227 | layout.add(object())
228 | layout.add(object())
229 |
--------------------------------------------------------------------------------
/tests/test_node.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from pytest import approx
3 |
4 | from plasma.debug import draw, info # noqa
5 | from plasma.node import Node, HORIZONTAL, AddMode, NotRestorableError
6 |
7 | from .conftest import Nodes
8 |
9 |
10 | class TestNode:
11 |
12 | def test_single_node(self):
13 | n = Node(None, 0, 0, 120, 50)
14 | assert n.x == 0
15 | assert n.y == 0
16 | assert n.width == 120
17 | assert n.height == 50
18 | assert n.is_root is True
19 | assert n.is_leaf is True
20 | assert n.parent is None
21 | assert n.children == []
22 | assert n.orient == HORIZONTAL
23 | assert n.horizontal is True
24 | assert n.vertical is False
25 | assert n.size is None
26 | assert (n.x, n.y) == n.pos
27 |
28 | def test_add_child(self, root):
29 | child = Node('a')
30 | root.add_child(child)
31 | assert root.children == [child]
32 | assert child.parent == root
33 | assert root.width == child.width == 120
34 | assert root.height == child.height == 50
35 | assert root.x == child.x == 0
36 | assert root.y == child.y == 0
37 |
38 | def test_add_children(self, root):
39 | a, b = Nodes('a b')
40 | root.add_child(a)
41 | root.add_child(b)
42 | assert root.width == 120
43 | assert a.width == b.width == 60
44 | assert root.height == 50
45 | assert a.height == b.height == 50
46 | assert a.pos == (0, 0)
47 | assert b.pos == (60, 0)
48 | c = Node('c')
49 | root.add_child(c)
50 | assert a.width == b.width == c.width == 40
51 | assert a.pos == (0, 0)
52 | assert b.pos == (40, 0)
53 | assert c.pos == (80, 0)
54 |
55 | def test_add_child_after(self, root, grid):
56 | a, b, c, d, e = grid
57 | f = Node('f')
58 | g = Node('g')
59 | h = Node('h')
60 | root.add_child_after(f, a)
61 | assert root.tree == [a, f, [b, [c, d, e]]]
62 | b.parent.add_child_after(g, b)
63 | assert root.tree == [a, f, [b, g, [c, d, e]]]
64 | d.parent.add_child_after(h, d)
65 | assert root.tree == [a, f, [b, g, [c, d, h, e]]]
66 |
67 | def test_add_child_after_with_sizes(self, root):
68 | a, b, c = Nodes('a b c')
69 | root.add_child(a)
70 | root.add_child(b)
71 | a.size += 10
72 | b.size += 10
73 | b.parent.add_child_after(c, b)
74 | assert a.size == b.size == c.size == 40
75 |
76 | def test_remove_child(self, root):
77 | a, b = Nodes('a b')
78 | root.add_child(a)
79 | root.add_child(b)
80 | root.remove_child(a)
81 | assert root.children == [b]
82 | root.remove_child(b)
83 | assert root.children == []
84 |
85 | def test_nested(self, root):
86 | a, b, c, d = Nodes('a b c d')
87 | root.add_child(a)
88 | root.add_child(b)
89 | b.flip_with(c)
90 | assert a.width == 60
91 | assert b.width == 60
92 | assert c.width == 60
93 | assert a.height == 50
94 | assert b.height == 25
95 | assert c.height == 25
96 | b.flip_with(d)
97 | assert b.width == 30
98 | assert d.width == 30
99 |
100 | def test_leaves(self, root, grid):
101 | a, b, c, d, e = grid
102 | assert root.first_leaf is a
103 | assert root.last_leaf is e
104 | assert b.parent.first_leaf is b
105 | assert b.parent.last_leaf is e
106 |
107 | def test_directions(self, root, grid):
108 | a, b, c, d, e = grid
109 | assert a.up is None
110 | assert a.right is b
111 | assert a.down is None
112 | assert a.left is None
113 |
114 | assert b.up is None
115 | assert b.right is None
116 | assert b.down is c
117 | assert b.left is a
118 |
119 | assert c.up is b
120 | assert c.right is d
121 | assert c.down is None
122 | assert c.left is a
123 |
124 | assert d.up is b
125 | assert d.right is e
126 | assert d.down is None
127 | assert d.left is c
128 |
129 | assert e.up is b
130 | assert e.right is None
131 | assert e.down is None
132 | assert e.left is d
133 |
134 | def test_prev_next(self, grid):
135 | a, b, c, d, e = grid
136 | assert a.next_leaf == b
137 | assert b.next_leaf == c
138 | assert c.next_leaf == d
139 | assert d.next_leaf == e
140 | assert e.next_leaf == a
141 | assert a.prev_leaf == e
142 | assert e.prev_leaf == d
143 | assert d.prev_leaf == c
144 | assert c.prev_leaf == b
145 | assert b.prev_leaf == a
146 |
147 | def test_siblings(self, grid):
148 | a, b, c, d, e = grid
149 | assert d.siblings == [c, e]
150 | assert b.siblings == [c.parent]
151 |
152 | def test_move_forward(self, root, grid):
153 | a, b, c, d, e = grid
154 | assert c.parent.children == [c, d, e]
155 | c.move_right()
156 | assert c.parent.children == [d, c, e]
157 | c.move_right()
158 | assert c.parent.children == [d, e, c]
159 | c.move_right()
160 | assert root.tree == [a, [b, [d, e]], c]
161 |
162 | def test_move_backward(self, root, grid):
163 | a, b, c, d, e = grid
164 | e.move_left()
165 | assert c.parent.children == [c, e, d]
166 | e.move_left()
167 | assert c.parent.children == [e, c, d]
168 | e.move_left()
169 | assert root.tree == [a, e, [b, [c, d]]]
170 |
171 | def test_advanced_move(self, grid):
172 | a, b, c, d, e = grid
173 | c.move_up()
174 | assert b.parent.tree == [b, c, [d, e]]
175 | a.move_up()
176 | assert b.parent.tree == [b, c, [d, e]]
177 |
178 | def test_advanced_move2(self, root, grid):
179 | a, b, c, d, e = grid
180 | res = c.move_down()
181 | assert b.parent.tree == [b, [d, e], c]
182 | assert res is True
183 | res = e.move_down()
184 | assert b.parent.tree == [b, d, e, c]
185 | assert res is True
186 | res = e.move_left()
187 | assert root.tree == [a, e, [b, d, c]]
188 | assert res is True
189 | res = d.move_right()
190 | assert root.tree == [a, e, [b, c], d]
191 | assert res is True
192 | res = a.move_left()
193 | assert root.tree == [a, e, [b, c], d]
194 | assert res is False
195 | res = d.move_right()
196 | assert root.tree == [a, e, [b, c], d]
197 | assert res is False
198 |
199 | def test_move_blocked(self, root, grid):
200 | a, b, c, d, e = grid
201 | orig_tree = root.tree.copy()
202 | res = a.move_up()
203 | assert root.tree == orig_tree
204 | assert res is False
205 | res = b.move_up()
206 | assert root.tree == orig_tree
207 | assert res is False
208 |
209 | def test_move_root(self, root):
210 | a = Node('a')
211 | root.add_child(a)
212 | root.move_up()
213 | assert root.tree == [a]
214 |
215 | def test_integrate(self, root):
216 | a, b, c, d, e = Nodes('a b c d e')
217 | root.add_child(a)
218 | root.add_child(b)
219 | root.add_child(c)
220 | root.add_child(d)
221 | c.integrate_left()
222 | assert root.tree == [a, [b, c], d]
223 | a.integrate_right()
224 | assert root.tree == [[b, c, a], d]
225 | a.parent.add_child(e)
226 | c.integrate_down()
227 | assert root.tree == [[b, [a, c], e], d]
228 | e.integrate_up()
229 | assert root.tree == [[b, [a, c, e]], d]
230 |
231 | def test_integrate_nested(self, root, grid):
232 | a, b, c, d, e = grid
233 | c.integrate_right()
234 | assert root.tree == [a, [b, [[d, c], e]]]
235 |
236 | def test_move_and_integrate(self, root, grid):
237 | a, b, c, d, e = grid
238 | c.integrate_left()
239 | assert root.tree == [[a, c], [b, [d, e]]]
240 | a.integrate_right()
241 | assert root.tree == [c, [b, [d, e], a]]
242 | d.integrate_down()
243 | assert root.tree == [c, [b, e, [a, d]]]
244 | a.integrate_up()
245 | assert root.tree == [c, [b, [e, a], d]]
246 | e.integrate_left()
247 | assert root.tree == [[c, e], [b, a, d]]
248 | f = Node('f')
249 | a.flip_with(f)
250 | g = Node('g')
251 | a.flip_with(g)
252 | g.integrate_left()
253 | assert root.tree == [[c, e, g], [b, [a, f], d]]
254 |
255 | def test_impossible_integrate(self, root, grid):
256 | a, b, c, d, e = grid
257 | orig_tree = root.tree.copy()
258 | a.integrate_left()
259 | assert orig_tree == root.tree
260 | b.integrate_up()
261 | assert orig_tree == root.tree
262 |
263 | def test_impossible_integrate2(self, root):
264 | a, b = Nodes('a b')
265 | root.add_child(a)
266 | root.add_child(b)
267 | orig_tree = root.tree.copy()
268 | b.integrate_up()
269 | assert root.tree == orig_tree
270 | b.integrate_down()
271 | assert root.tree == orig_tree
272 | b.integrate_right()
273 | assert root.tree == orig_tree
274 | a.integrate_up()
275 | assert root.tree == orig_tree
276 | a.integrate_down()
277 | assert root.tree == orig_tree
278 | a.integrate_left()
279 | assert root.tree == orig_tree
280 |
281 | def test_find_payload(self, root, grid):
282 | a, b, c, d, e = grid
283 | assert root.find_payload('a') is a
284 | assert root.find_payload('b') is b
285 | assert root.find_payload('d') is d
286 | assert root.find_payload('x') is None
287 |
288 | def test_last_access(self, grid):
289 | a, b, c, d, e = grid
290 | f = Node('f')
291 | a.flip_with(f)
292 | d.access()
293 | assert b.down is d
294 | b.access()
295 | assert f.right is b
296 | f.access()
297 | assert b.left is f
298 |
299 | def test_root_without_dimensions(self):
300 | """A root node with undef. dimensions should be able to add a child."""
301 | root = Node()
302 | x = Node('x')
303 | root.add_child(x)
304 |
305 | def test_root(self, root, grid):
306 | for node in grid:
307 | assert node.root is root
308 |
309 | def test_all(self, root, grid):
310 | assert set(root.all_leafs) == set(grid)
311 |
312 | def test_close_neighbor(self, root):
313 | a, b, c, d = Nodes('a b c d')
314 | root.add_child(a)
315 | root.add_child(b)
316 | a.flip_with(c)
317 | b.flip_with(d)
318 | assert a.close_up is None
319 | assert a.close_left is None
320 | assert a.close_right is b
321 | assert a.close_down is c
322 |
323 | assert b.close_up is None
324 | assert b.close_left is a
325 | assert b.close_right is None
326 | assert b.close_down is d
327 |
328 | assert c.close_up is a
329 | assert c.close_left is None
330 | assert c.close_right is d
331 | assert c.close_down is None
332 |
333 | assert d.close_up is b
334 | assert d.close_left is c
335 | assert d.close_right is None
336 | assert d.close_down is None
337 |
338 | def test_close_neighbor2(self, root, small_grid):
339 | a, b, c, d = small_grid
340 | assert b.close_left is a
341 |
342 | def test_close_neighbor_nested(self, root, grid):
343 | a, b, c, d, e = grid
344 | f, g, h, i, j, k, L = Nodes('f g h i j k l')
345 | root.add_child(f)
346 | d.flip_with(h)
347 | a.flip_with(i)
348 | e.flip_with(j)
349 | e.parent.add_child(k)
350 | f.flip_with(L)
351 | f.height = 10
352 | assert b.close_down is d
353 | b.flip_with(g)
354 | assert b.close_down is c
355 | assert d.close_right is e
356 | assert e.close_left is d
357 | assert L.close_left is e
358 | assert e.close_up is g
359 | assert L.close_right is None
360 | assert h.close_down is None
361 |
362 | def test_close_neighbor_approx(self, root, small_grid):
363 | """Tolerate floating point errors when calculating common borders."""
364 | root.height += 30
365 | a, b, c, d = small_grid
366 | e, f, g = Nodes('e f g')
367 | c.flip_with(f)
368 | b.parent.add_child(e)
369 | c.parent.add_child(g)
370 | assert g.close_down is e
371 |
372 | def test_points(self, root, small_grid):
373 | a, b, c, d = small_grid
374 | assert c.top_left == (60, 25)
375 | assert c.top_right == (90, 25)
376 | assert c.bottom_left == (60, 50)
377 | assert c.bottom_right == (90, 50)
378 |
379 | def test_center(self, root):
380 | assert root.x_center == 60
381 | assert root.y_center == 25
382 | assert root.center == (60, 25)
383 |
384 | def test_recent_leaf(self, root, grid):
385 | a, b, c, d, e = grid
386 | assert d.parent.recent_leaf is c
387 | c.access()
388 | d.access()
389 | assert d.parent.recent_leaf is d
390 | b.access()
391 | c.access()
392 | assert root.recent_leaf is c
393 | a.access()
394 | assert root.recent_leaf is a
395 |
396 | def test_recent_close_neighbor(self, root, grid):
397 | a, b, c, d, e = grid
398 | assert b.close_down is d
399 | c.access()
400 | assert b.close_down is c
401 | assert a.close_right is c
402 | b.access()
403 | assert a.close_right is b
404 |
405 | def test_add_node(self, root):
406 | a, b, c, d, e, f, g = Nodes('a b c d e f g')
407 | root.add_node(a)
408 | assert root.tree == [a]
409 | root.add_node(b)
410 | assert root.tree == [a, b]
411 | a.add_node(c)
412 | assert root.tree == [a, c, b]
413 | c.add_node(d, mode=AddMode.HORIZONTAL)
414 | assert root.tree == [a, c, d, b]
415 | root.remove_child(d)
416 | c.add_node(d, mode=AddMode.VERTICAL)
417 | c.parent.add_child_after
418 | assert root.tree == [a, [c, d], b]
419 | c.add_node(e, mode=AddMode.VERTICAL)
420 | assert root.tree == [a, [c, e, d], b]
421 | assert a.width == 40
422 | a.add_node(f, mode=AddMode.HORIZONTAL | AddMode.SPLIT)
423 | assert root.tree == [a, f, [c, e, d], b]
424 | assert a.width == f.width == 20
425 | assert c.parent.width == b.width == 40
426 | a.add_node(g, mode=AddMode.VERTICAL | AddMode.SPLIT)
427 | assert root.tree == [[a, g], f, [c, e, d], b]
428 |
429 | def test_contains(self, root, grid):
430 | x = Node('x')
431 | nodes = list(grid)
432 | nodes += [n.parent for n in nodes]
433 | nodes.append(root)
434 | for n in nodes:
435 | assert n in root
436 | assert x not in root
437 |
438 | class TestSizes:
439 |
440 | def test_size(self, grid):
441 | a, b, c, d, e = grid
442 | assert a.size == a.width == 60
443 | assert b.size == b.height == 25
444 |
445 | def test_capacity(self, root, grid):
446 | a, b, c, d, e = grid
447 | assert root.capacity == 120
448 | assert b.parent.capacity == 50
449 | assert c.parent.capacity == 60
450 | assert c.capacity == 25
451 |
452 | def test_capacity2(self, root):
453 | a, b, c = Nodes('a b c')
454 | root.add_child(a)
455 | root.add_child(b)
456 | b.flip_with(c)
457 |
458 | def test_resize(self, root, grid):
459 | a, b, c, d, e = grid
460 | a.size += 10
461 | assert a.width == a.size == 70
462 | assert b.height == b.size == 25
463 | assert b.width == 50
464 | assert c.width == d.width == e.width == 50/3
465 | assert a.pos == (0, 0)
466 | assert b.pos == (70, 0)
467 | assert c.pos == (70, 25)
468 | assert d.pos == (70 + 50/3, 25)
469 | assert e.pos == (70 + (50/3)*2, 25)
470 | b.size -= 5
471 | assert c.width == d.width == e.width == 50/3
472 | assert c.height == d.height == e.height == 30
473 | d.size += 5
474 | assert d.width == 50/3 + 5
475 | d.move_up()
476 | assert d.size == (50 - b.size) / 2
477 | b.integrate_down()
478 | assert b.size == d.size == 25
479 | assert b.parent.size == 25
480 |
481 | def test_resize_absolute(self, grid):
482 | a, b, c, d, e = grid
483 | b.size = 10
484 | assert b.size == b.height == 10
485 | assert c.parent.size == 40
486 | b.size = 5
487 | assert b.size == 10
488 |
489 | def test_resize_absolute2(self, root):
490 | a, b, c = Nodes('a b c')
491 | root.add_child(a)
492 | root.add_child(b)
493 | root.add_child(c)
494 | a.size = 30
495 | b.size = 60
496 | c.size = 40
497 | assert a.size == 30 * (80/90)
498 | assert b.size == 60 * (80/90)
499 | assert c.size == 40
500 |
501 | def test_resize_absolute_and_relative(self, root):
502 | a, b, c, d = Nodes('a b c d')
503 | root.add_child(a)
504 | root.add_child(b)
505 | a.size = 20
506 | b.size = 20
507 | assert a.size == 100
508 | assert b.size == 20
509 | root.add_child(c)
510 | assert c.size == approx(40)
511 | assert a.size == approx(100 * (2/3))
512 | assert b.size == approx(20 * (2/3))
513 | root.add_child(d)
514 | assert c.size == d.size == approx(20)
515 |
516 | def test_resize_absolute_and_relative2(self, root):
517 | a, b, c = Nodes('a b c')
518 | root.add_child(a)
519 | root.add_child(b)
520 | root.add_child(c)
521 | a.size += 10
522 | assert a.size == 50
523 | assert b.size == 35
524 | assert c.size == 35
525 | b.size += 10
526 | assert a.size == 50
527 | assert b.size == 45
528 | assert c.size == 25
529 |
530 | def test_resize_flat(self, root):
531 | a, b, c, d, e, f = Nodes('a b_abs c d e_abs f_abs')
532 | root.add_child(a)
533 | root.add_child(b)
534 | root.add_child(c)
535 | root.add_child(d)
536 | d.flip_with(e)
537 | e.flip_with(f)
538 | b.size = b.size
539 | e.size = e.size
540 | f.size = f.size
541 | a.size = 60
542 | assert a.size == 60
543 | assert b.size == 25
544 | assert c.size == 10
545 | assert d.parent.size == 25
546 | assert e.size == f.size == 25/2
547 |
548 | def test_resize_minimum(self, grid):
549 | a, b, c, d, e = grid
550 | b.size -= 100
551 | assert b.size == 10
552 |
553 | def test_resize_all_absolute_underflow(self, root, grid):
554 | a, b, c, d, e = grid
555 | c.size = 10
556 | d.size = 10
557 | assert e.size == 40
558 | e.size = 10
559 | assert e.size == 10
560 | assert c.size == d.size == 25
561 |
562 | def test_resize_all_absolute_overflow(self, grid):
563 | a, b, c, d, e = grid
564 | c.size = d.size = 15
565 | e.size = 40
566 | assert e.size == 40
567 | assert c.size == d.size == 10
568 | e.size = 50
569 | assert e.size == 40
570 | assert c.size == d.size == 10
571 |
572 | def test_resize_overflow_with_relative(self, root, grid):
573 | a, b, c, d, e = grid
574 | c.size = 20
575 | d.size = 40
576 | assert c.size == 10
577 | assert d.size == 40
578 | assert e.size == 10
579 | assert e.flexible
580 | d.size = 50
581 | assert c.size == 10
582 | assert d.size == 40
583 | assert e.size == 10
584 | assert e.flexible
585 |
586 | def test_resize_overflow_with_relative2(self, root, grid):
587 | a, b, c, d, e = grid
588 | c.size = 20
589 | d.size = 20
590 | a.size = 70
591 | assert a.size == 70
592 | assert c.size == d.size == 20
593 | assert e.size == 10
594 | a.size = 80
595 | assert a.size == 80
596 | assert c.size == d.size == 15
597 | assert e.size == 10
598 | a.size = 90
599 | assert a.size == 90
600 | assert c.size == d.size == e.size == 10
601 | a.size = 100
602 | assert a.size == 90
603 |
604 | def test_resize_only_absolute_remains(self, root):
605 | a, b, c = Nodes('a b c')
606 | root.add_child(a)
607 | root.add_child(b)
608 | a.size = 20
609 | b.size = 20
610 | root.add_child(c)
611 | root.remove_child(c)
612 | assert a.size == 100
613 | assert b.size == 20
614 |
615 | def test_reset_size(self, grid):
616 | a, b, c, d, e = grid
617 | a.size += 5
618 | assert a.size == 65
619 | a.reset_size()
620 | assert a.size == 60
621 |
622 | def test_size_after_split(self, root):
623 | a, b, c = Nodes('a b c')
624 | root.add_child(a)
625 | root.add_child(b)
626 | b.size -= 20
627 | b.flip_with(c)
628 | assert b.parent.size == 40
629 | assert b.size == c.size == 25
630 | b.remove()
631 | assert c.size == 40
632 |
633 | def test_only_child_must_be_flexible(self, root):
634 | a, b = Nodes('a b')
635 | root.add_child(a)
636 | root.add_child(b)
637 | a.size = 10
638 | root.remove_child(b)
639 | assert a.flexible
640 |
641 | def test_deny_only_child_resize(self, root):
642 | a = Node('a')
643 | root.add_child(a)
644 | a.size = 10
645 | assert a.size == 120
646 |
647 | def test_resize_parents(self, root):
648 | a, b, c = Nodes('a b c')
649 | root.add_child(a)
650 | root.add_child(b)
651 | b.flip_with(c)
652 | b.width += 10
653 | assert b.parent.size == 70
654 | assert b.size == c.size == 25
655 |
656 | def test_pixelperfect(self, root, tiny_grid):
657 | a, b, c = tiny_grid
658 | root._height = 11
659 | root._width = 11
660 | ds = a.pixel_perfect
661 | assert all(type(x) is int for x in (ds.x, ds.y, ds.width, ds.height))
662 | assert a.width + b.width == 11
663 | assert a.pixel_perfect.width + b.pixel_perfect.width == 11
664 | assert b.height + c.height == 11
665 | assert b.pixel_perfect.height + c.pixel_perfect.height == 11
666 |
667 | def test_pixelperfect_draw(self, root, complex_grid):
668 | root._height = 10
669 | for i in range(40, 50):
670 | root._width = i
671 | view = draw(root)
672 | assert '#' not in view
673 | root._width = 50
674 | for i in range(10, 20):
675 | root._height = i
676 | view = draw(root)
677 | assert '#' not in view
678 |
679 | def test_resize_root(self, root):
680 | a, b, c = Nodes('a b c')
681 | root.add_child(a)
682 | root.add_child(b)
683 | a.height += 10
684 | root.height += 10
685 | root.width += 10
686 | root.size = 10
687 | assert a._size is b._size is root._size is None
688 |
689 | def test_set_xy(self, root, tiny_grid):
690 | a, b, c = tiny_grid
691 | root.x = 10
692 | root.y = 20
693 | assert root.x == 10
694 | assert root.y == 20
695 | a.x = 30
696 | a.y = 40
697 | assert a.x == root.x == 10
698 | assert a.y == root.y == 20
699 | root.width = 50
700 | root.height = 60
701 | assert root._width == 50
702 | assert root._height == 60
703 |
704 | def test_set_width_height(self, root, tiny_grid):
705 | a, b, c = tiny_grid
706 | a.width = 70
707 | assert a.width == 70
708 | assert b.width == c.width == 50
709 | b.height = 30
710 | assert b.height == 30
711 | assert c.height == 20
712 | b.width = 80
713 | assert b.width == c.width == 80
714 | assert a.width == 40
715 | a.height = 20
716 | assert a.height == 50
717 |
718 | def test_min_size(self, root, small_grid):
719 | a, b, c, d = small_grid
720 | c.size += 10
721 | d.size += 20
722 | b.size = 20
723 | assert a.min_size == Node.min_size_default
724 | assert b.parent.min_size == 60
725 | assert b.min_size == 20
726 | assert c.parent.min_size == Node.min_size_default
727 | assert c.min_size == 20
728 | assert d.min_size == 40
729 |
730 | def test_transitive_flexible(self, root, complex_grid):
731 | a, b, c, d, e, f, g = complex_grid
732 | assert b.parent.flexible
733 | d.size = 20
734 | e.size = 20
735 | f.size = 10
736 | assert b.parent.flexible
737 | g.size = 10
738 | assert not b.parent.flexible
739 |
740 | def test_resize_bubbles(self, root, small_grid):
741 | a, b, c, d = small_grid
742 | c.size += 10
743 | d.size += 20
744 | assert c.size == 20
745 | assert d.size == 40
746 | a.size = 30
747 | assert c.size == 30
748 | assert d.size == 60
749 |
750 | def test_resize_bubbles2(self, root, complex_grid):
751 | a, b, c, d, e, f, g = complex_grid
752 | c.flip_with(Node('h'))
753 | f.size += 10
754 | g.size += 10
755 | assert f.size == g.size == 10
756 | assert f.fixed and g.fixed
757 | assert d.size == e.size == 20
758 | assert d.flexible and e.flexible
759 | a.size -= 40
760 | assert a.size == 20
761 | assert f.size == g.size == 10
762 | assert d.size == e.size == 40
763 | d.size = 10
764 | assert d.size == 10
765 | assert e.size == 70
766 | assert f.size == g.size == 10
767 | assert e.flexible
768 | e.size = 10
769 | assert e.fixed
770 |
771 | def test_resize_bubbles3(self, root, complex_grid):
772 | a, b, c, d, e, f, g = complex_grid
773 | h = Node('h')
774 | c.flip_with(h)
775 | f.size += 10
776 | g.size += 10
777 | assert f.size == g.size == c.size == h.size == 10
778 | a.size = 10
779 | assert a.size == 10
780 | assert f.size == g.size == c.size == h.size == 10
781 | assert d.size == e.size == 45
782 | d.size = 10
783 | assert d.size == 10
784 | assert e.size == 80
785 | e.size = 10
786 | assert e.size == 10
787 | assert f.size == g.size == c.size == h.size == d.size == 100/3
788 |
789 | def test_resize_nested(self, root):
790 | a, b, c, d, e, f, g, h = Nodes('a b c_abs d_abs e f g h_abs')
791 | nu1, nu2, nd, mu, md1, md2 = Nodes('nu1_abs nu2_abs nd mu md1_abs md2')
792 | ou1, ou2, od, pu, pd1, pd2 = Nodes('ou1_abs ou2_abs od pu pd1_abs '
793 | 'pd2_abs')
794 | root.add_child(a)
795 | root.add_child(b)
796 | b.flip_with(c)
797 | b.parent.add_child(e)
798 | b.parent.add_child(g)
799 | c.flip_with(d)
800 | e.flip_with(f)
801 | g.flip_with(h)
802 |
803 | b.parent.add_child(nu1)
804 | nu1.flip_with(mu)
805 | nu1.flip_with(nd)
806 | nu1.flip_with(nu2)
807 | mu.flip_with(md1)
808 | md1.flip_with(md2)
809 |
810 | b.parent.add_child(ou1)
811 | ou1.flip_with(pu)
812 | ou1.flip_with(od)
813 | ou1.flip_with(ou2)
814 | pu.flip_with(pd1)
815 | pd1.flip_with(pd2)
816 |
817 | def assert_first_state():
818 | assert b.parent.size == 60
819 | assert c.size == 40
820 | assert d.size == 20
821 | assert e.size == f.size == 30
822 | assert g.size == 40
823 | assert h.size == 20
824 | assert nu1.size == 10
825 | assert nu2.size == 20
826 | assert nd.parent.size == 30
827 | assert mu.parent.size == 30
828 | assert md1.size == 10
829 | assert md2.size == 20
830 | assert ou1.size == 10
831 | assert ou2.size == 20
832 | assert od.parent.size == 30
833 | assert pu.parent.size == 30
834 | assert pd1.size == 10
835 | assert pd2.size == 20
836 |
837 | def assert_second_state():
838 | assert a.size == 30
839 | assert b.parent.size == 90
840 | assert c.size == 60
841 | assert d.size == 30
842 | assert e.size == f.size == 45
843 | assert g.size == 70
844 | assert h.size == 20
845 | assert nu1.size == 10
846 | assert nu2.size == 20
847 | assert nd.parent.size == 30
848 | assert mu.parent.size == 60
849 | assert md1.size == 10
850 | assert md2.size == 50
851 | assert ou1.size == 15
852 | assert ou2.size == 30
853 | assert od.parent.size == 45
854 | assert pd1.size == 15
855 | assert pd2.size == 30
856 | assert pu.parent.size == 45
857 |
858 | b.parent.size = 60
859 | c.size += 5
860 | d.size -= 5
861 | h.size = 20
862 | nu1.size = 10
863 | nu2.size = 20
864 | md1.size = 10
865 | ou1.size = 10
866 | ou2.size = 20
867 | pd1.size = 10
868 | pd2.size = 20
869 |
870 | assert a.size == 60
871 | assert_first_state()
872 | a.size -= 30
873 | assert_second_state()
874 | a.size += 30
875 | assert a.size == 60
876 | assert_first_state()
877 | b.parent.size += 30
878 | assert_second_state()
879 | b.parent.size -= 30
880 | assert a.size == 60
881 | assert_first_state()
882 |
883 | a.size = 30
884 | x = Node('x')
885 | root.add_child(x)
886 | assert x.size == 40
887 | assert_first_state()
888 | x.remove()
889 | assert_second_state()
890 |
891 | a.remove()
892 | assert b.width == 120
893 | y = Node('y')
894 | root.add_child(y)
895 | assert_first_state()
896 |
897 | def test_resize_max(self, root, tiny_grid):
898 | a, b, c = tiny_grid
899 | a.width = 120
900 | assert a.width == 110
901 | assert b.width == c.width == 10
902 |
903 | class TestRestore:
904 |
905 | def test_restore(self, root, grid):
906 | a, b, c, d, e = grid
907 | tree = root.tree
908 | for node in grid:
909 | node.remove()
910 | root.restore(node)
911 | assert root.tree == tree
912 |
913 | def test_restore_same_payload(self, root, grid):
914 | """Restore a node that's not identical with the removed one but carries
915 | the same payload.
916 | """
917 | a, b, c, d, e = grid
918 | d.remove()
919 | new_d = Node('d')
920 | root.restore(new_d)
921 | assert root.tree == [a, [b, [c, new_d, e]]]
922 |
923 | def test_restore_unknown(self, root, grid):
924 | a, b, c, d, e = grid
925 | with pytest.raises(NotRestorableError):
926 | root.restore(Node('x'))
927 | d.remove()
928 | with pytest.raises(NotRestorableError):
929 | root.restore(Node('x'))
930 | root.restore(d)
931 | assert root.tree == [a, [b, [c, d, e]]]
932 |
933 | def test_restore_no_parent(self, root, small_grid):
934 | a, b, c, d = small_grid
935 | c.remove()
936 | d.remove()
937 | with pytest.raises(NotRestorableError):
938 | root.restore(c)
939 | root.restore(d)
940 | assert root.tree == [a, [b, d]]
941 |
942 | def test_restore_bad_index(self, root, grid):
943 | a, b, c, d, e = grid
944 | f, g = Nodes('f g')
945 | e.parent.add_child(f)
946 | e.parent.add_child(g)
947 | g.remove()
948 | f.remove()
949 | e.remove()
950 | root.restore(g)
951 | assert root.tree == [a, [b, [c, d, g]]]
952 |
953 | def test_restore_sizes(self, root, grid):
954 | a, b, c, d, e = grid
955 | c.size = 30
956 | c.remove()
957 | root.restore(c)
958 | assert c.size == 30
959 | c.remove()
960 | d.size = 30
961 | e.size = 30
962 | assert d.size == e.size == 30
963 | root.restore(c)
964 | assert c.size == 30
965 | assert d.size == e.size == 15
966 |
967 | def test_restore_sizes_flip(self, root, tiny_grid):
968 | a, b, c = tiny_grid
969 | c.size = 10
970 | c.remove()
971 | assert a._size is b._size is None
972 | root.restore(c)
973 | assert c.size == 10
974 | b.size = 10
975 | c.remove()
976 | root.restore(c)
977 | assert b.size == 10
978 | b.remove()
979 | root.restore(b)
980 | assert b.size == 10
981 |
982 | def test_restore_root(self, root):
983 | a, b = Nodes('a b')
984 | root.add_child(a)
985 | root.add_child(b)
986 | a.size = 20
987 | a.remove()
988 | root.restore(a)
989 | assert a._size == 20
990 | assert b._size is None
991 | b.remove()
992 | root.restore(b)
993 | assert a._size == 20
994 | assert b._size is None
995 |
996 | def test_restore_root2(self, root):
997 | a, b, c = Nodes('a b c')
998 | root.add_child(a)
999 | root.add_child(b)
1000 | root.add_child(c)
1001 | b.size = 20
1002 | c.size = 40
1003 | a.remove()
1004 | assert b.size == 40
1005 | assert c.size == 80
1006 | root.restore(a)
1007 | assert not a.fixed
1008 | assert a.size == 60
1009 | assert b.size == 20
1010 | assert c.size == 40
1011 |
1012 | def test_restore_keep_flexible(self, root, tiny_grid):
1013 | a, b, c = tiny_grid
1014 | b.remove()
1015 | root.restore(b)
1016 | assert a._size is b._size is c._size is None
1017 | b.size = 10
1018 | b.remove()
1019 | root.restore(b)
1020 | assert b._size == 10
1021 | assert c._size is None
1022 | c.remove()
1023 | root.restore(c)
1024 | assert b._size == 10
1025 | assert c._size is None
1026 | c.size = 10
1027 | b.reset_size()
1028 | b.remove()
1029 | root.restore(b)
1030 | assert b._size is None
1031 | assert c._size == 10
1032 | c.remove()
1033 | root.restore(c)
1034 | assert b._size is None
1035 | assert c._size == 10
1036 |
1037 | def test_resize_with_collapse_and_restore(self, root, small_grid):
1038 | a, b, c, d = small_grid
1039 | root.height = 30
1040 | c.size = 30
1041 | d.size += 10
1042 | b.remove()
1043 | assert c.size == c.height == 10
1044 | assert d.size == d.height == 20
1045 | root.restore(b)
1046 | assert b.height == 15
1047 | assert b.width == 60
1048 | assert c.height == d.height == 15
1049 | assert c.width == 20
1050 | assert d.width == 40
1051 |
--------------------------------------------------------------------------------
/tools/make_readme.py:
--------------------------------------------------------------------------------
1 | """Update documentation in readme.
2 |
3 | This tool extracts the documentation (docstrings) for all layout commands
4 | (startings with "cmd_") from the layout class and inserts them as a table into
5 | the readme file, in the marked area.
6 |
7 | It should be run with every API change.
8 | """
9 |
10 | import ast
11 | import re
12 |
13 |
14 | readme_path = 'README.md'
15 | layout_path = 'plasma/layout.py'
16 | marker = '\n'
17 | start_marker = marker.format(pos='start')
18 | end_marker = marker.format(pos='end')
19 |
20 | def table(text):
21 | return '\n' % text
22 |
23 | def row(text):
24 | return ' \n%s
\n' % text
25 |
26 | def col(text):
27 | return ' %s | \n' % text
28 |
29 | def code(text):
30 | return '%s
' % text
31 |
32 | def function_name(name, args):
33 | return '%s(%s)' % (name, ', '.join(args))
34 |
35 | def function_desc(text):
36 | return re.sub('`([^`]*)`', code('\\1'), text.replace('\n\n', '
\n'))
37 |
38 | def main():
39 | with open(readme_path) as f:
40 | text = f.read()
41 | text_pre, rest = text.split(start_marker)
42 | _, text_post = rest.split(end_marker)
43 |
44 | with open(layout_path) as f:
45 | tree = ast.parse(f.read())
46 |
47 | rows = ''
48 | for node in ast.walk(tree):
49 | if not isinstance(node, ast.FunctionDef):
50 | continue
51 | if not node.name.startswith('cmd_'):
52 | continue
53 | name = node.name[4:]
54 | args = [a.arg for a in node.args.args[1:]]
55 | docstring = ast.get_docstring(node)
56 | rows += row(
57 | col(code(function_name(name, args))) +
58 | col(function_desc(docstring))
59 | )
60 | text_table = table(rows)
61 |
62 | with open(readme_path, 'w') as f:
63 | f.write(text_pre + start_marker + text_table + end_marker + text_post)
64 | print('Commands written to "%s".' % readme_path)
65 |
66 | if __name__ == '__main__':
67 | main()
68 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py37,py38,py39,lint,coverage-report
3 |
4 | [testenv]
5 | deps =
6 | setuptools>=41
7 | xcffib
8 | coverage
9 | pytest<5.0.0
10 | pytest-xdist
11 | commands =
12 | coverage run --parallel -m pytest -v {posargs} tests/
13 |
14 | [testenv:coverage-report]
15 | basepython = python3.9
16 | skip_install = true
17 | deps = coverage
18 | commands =
19 | coverage combine
20 | coverage report
21 |
22 | [testenv:lint]
23 | deps =
24 | xcffib
25 | flake8
26 | pylint
27 | commands =
28 | flake8 plasma/
29 | pylint --rcfile setup.cfg plasma/
30 |
31 | [testenv:release]
32 | deps =
33 | wheel
34 | twine
35 | commands =
36 | rm -rf *.egg-info build/ dist/
37 | python setup.py bdist_wheel sdist
38 | twine upload -r pypi dist/*
39 | rm -rf *.egg-info build/ dist/
40 |
41 | [pylint]
42 | disable =
43 | missing-docstring,
44 | invalid-name,
45 | unused-argument,
46 | too-few-public-methods,
47 | too-many-public-methods,
48 | protected-access,
49 | no-self-use,
50 | too-many-instance-attributes,
51 | fixme,
52 | raise-missing-from,
53 |
--------------------------------------------------------------------------------