Merge pull request #1943 from KLayout/hotfix-0.29.10

Hotfix 0.29.10
This commit is contained in:
Matthias Köfferlein 2024-12-03 19:02:04 +01:00 committed by GitHub
commit 9441024fd0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 173 additions and 101 deletions

View File

@ -1,3 +1,13 @@
0.29.10 (2024-12-03):
* Bug: %GITHUB%/issues/1941 Crash with the navigator open
* Bug: %GITHUB%/issues/1942 Syntax error in pyi stubs
As a bonus, added defaults for Box#enlarge and Box#enlarged (dx, dy)
* Bugfix: Partial mode snapping now is object first, then grid
* Bugfix: Key bindings have not been properly read from the configuration file
The change in the configuration string structure triggered an old bug:
Toolbar buttons had a twofold configuration and only the last one was
considered. Changing the order of the entries could spoil the configuration.
0.29.9 (2024-12-01):
* Bug: %GITHUB%/issues/1907 Locking layouts against modification during recursive iteration of instances and shapes
This prevents crashes in write-white-iterating scenarios

View File

@ -1,3 +1,10 @@
klayout (0.29.10-1) unstable; urgency=low
* New features and bugfixes
- See changelog
-- Matthias Köfferlein <matthias@koefferlein.de> Mon, 02 Dec 2024 22:23:47 +0100
klayout (0.29.9-1) unstable; urgency=low
* New features and bugfixes

View File

@ -4,6 +4,7 @@
# clean up
rm -rf build dist
rm -rf python3-venv-make_stubs
python3 setup.py build
python3 setup.py bdist_wheel

View File

@ -388,7 +388,7 @@ struct box_defs
"\n"
"@return A reference to this box.\n"
) +
method_ext ("moved", &box_defs<C>::moved, gsi::arg ("dx, 0"), gsi::arg ("dy", 0),
method_ext ("moved", &box_defs<C>::moved, gsi::arg ("dx", 0), gsi::arg ("dy", 0),
"@brief Moves the box by a certain distance\n"
"\n"
"This is a convenience method which takes two values instead of a Point object.\n"
@ -419,7 +419,7 @@ struct box_defs
"\n"
"@return The moved box.\n"
) +
method_ext ("enlarge", &box_defs<C>::enlarge, gsi::arg ("dx"), gsi::arg ("dy"),
method_ext ("enlarge", &box_defs<C>::enlarge, gsi::arg ("dx", 0), gsi::arg ("dy", 0),
"@brief Enlarges the box by a certain amount.\n"
"\n"
"\n"
@ -436,7 +436,7 @@ struct box_defs
"\n"
"@return A reference to this box.\n"
) +
method_ext ("enlarged", &box_defs<C>::enlarged, gsi::arg ("dx"), gsi::arg ("dy"),
method_ext ("enlarged", &box_defs<C>::enlarged, gsi::arg ("dx", 0), gsi::arg ("dy", 0),
"@brief Enlarges the box by a certain amount.\n"
"\n"
"\n"

View File

@ -1808,11 +1808,38 @@ PartialService::mouse_move_event (const db::DPoint &p, unsigned int buttons, boo
// for a single selected point or edge, m_start is the original position and we snap the target -
// thus, we can bring the point on grid or to an object's edge or vertex
snap_details = snap2 (p);
if (snap_details.object_snap == lay::PointSnapToObjectResult::NoObject) {
m_current = m_start + snap_move (p - m_start);
} else {
m_current = m_start + snap_move (snap_details.snapped_point - m_start);
auto snapped_to_object = snap_details.snapped_point;
m_current = snapped_to_object;
if (snap_details.object_snap != lay::PointSnapToObjectResult::ObjectVertex) {
// snap to grid on longer side of reference edge and to object on shorter
auto snapped_to_object_and_grid = m_start + snap_move (snapped_to_object - m_start);
if (std::abs (snap_details.object_ref.dx ()) > std::abs (snap_details.object_ref.dy ())) {
m_current.set_x (snapped_to_object_and_grid.x ());
// project to edge, so we always hit it
auto cp = snap_details.object_ref.cut_point (db::DEdge (m_current, m_current + db::DVector (0, 1.0)));
if (cp.first) {
m_current.set_y (cp.second.y ());
}
} else if (std::abs (snap_details.object_ref.dy ()) > std::abs (snap_details.object_ref.dx ())) {
m_current.set_y (snapped_to_object_and_grid.y ());
// project to edge, so we always hit it
auto cp = snap_details.object_ref.cut_point (db::DEdge (m_current, m_current + db::DVector (1.0, 0)));
if (cp.first) {
m_current.set_x (cp.second.x ());
}
}
}
mouse_cursor_from_snap_details (snap_details);
}
} else {

View File

@ -208,11 +208,6 @@ MacroController::uninitialize (lay::Dispatcher * /*root*/)
bool
MacroController::configure (const std::string &key, const std::string &value)
{
if (key == cfg_key_bindings) {
m_key_bindings = unpack_key_binding (value);
} else if (key == cfg_menu_items_hidden) {
m_menu_items_hidden = unpack_menu_items_hidden (value);
}
return false;
}
@ -810,21 +805,9 @@ MacroController::do_update_menu_with_macros ()
add_macro_items_to_menu (m_temp_macros, used_names, groups, tech);
add_macro_items_to_menu (lym::MacroCollection::root (), used_names, groups, tech);
// apply the custom keyboard shortcuts
for (std::vector<std::pair<std::string, std::string> >::const_iterator kb = m_key_bindings.begin (); kb != m_key_bindings.end (); ++kb) {
if (mp_mw->menu ()->is_valid (kb->first)) {
lay::Action *a = mp_mw->menu ()->action (kb->first);
a->set_shortcut (kb->second);
}
}
// apply the custom hidden flags
for (std::vector<std::pair<std::string, bool> >::const_iterator hf = m_menu_items_hidden.begin (); hf != m_menu_items_hidden.end (); ++hf) {
if (mp_mw->menu ()->is_valid (hf->first)) {
lay::Action *a = mp_mw->menu ()->action (hf->first);
a->set_hidden (hf->second);
}
}
// apply the custom keyboard shortcuts and hidden flags
mp_mw->apply_key_bindings ();
mp_mw->apply_hidden ();
}
void

View File

@ -249,8 +249,6 @@ private:
tl::DeferredMethod<MacroController> dm_do_sync_with_external_sources;
tl::DeferredMethod<MacroController> dm_sync_file_watcher;
tl::DeferredMethod<MacroController> dm_sync_files;
std::vector<std::pair<std::string, std::string> > m_key_bindings;
std::vector<std::pair<std::string, bool> > m_menu_items_hidden;
void sync_implicit_macros (bool ask_before_autorun);
void add_macro_items_to_menu (lym::MacroCollection &collection, std::set<std::string> &used_names, std::set<std::string> &groups, const db::Technology *tech);

View File

@ -1192,8 +1192,8 @@ MainWindow::configure (const std::string &name, const std::string &value)
} else if (name == cfg_menu_items_hidden) {
std::vector<std::pair<std::string, bool> > hidden = unpack_menu_items_hidden (value);
apply_hidden (hidden);
m_hidden = unpack_menu_items_hidden (value);
apply_hidden ();
return true;
} else if (name == cfg_initial_technology) {
@ -1213,12 +1213,15 @@ MainWindow::configure (const std::string &name, const std::string &value)
}
void
MainWindow::apply_hidden (const std::vector<std::pair<std::string, bool> > &hidden)
MainWindow::apply_hidden ()
{
for (std::vector<std::pair<std::string, bool> >::const_iterator hf = hidden.begin (); hf != hidden.end (); ++hf) {
if (menu ()->is_valid (hf->first)) {
lay::Action *a = menu ()->action (hf->first);
a->set_hidden (hf->second);
for (std::vector<std::pair<std::string, bool> >::const_iterator hf = m_hidden.begin (); hf != m_hidden.end (); ++hf) {
lay::AbstractMenuItem *item = menu ()->find_item_exact (hf->first);
if (item && item->primary ()) {
lay::Action *a = item->action ();
if (a) {
a->set_hidden (hf->second);
}
}
}
}
@ -1227,9 +1230,12 @@ void
MainWindow::apply_key_bindings ()
{
for (std::vector<std::pair<std::string, std::string> >::const_iterator kb = m_key_bindings.begin (); kb != m_key_bindings.end (); ++kb) {
if (menu ()->is_valid (kb->first)) {
lay::Action *a = menu ()->action (kb->first);
a->set_shortcut (kb->second);
lay::AbstractMenuItem *item = menu ()->find_item_exact (kb->first);
if (item && item->primary ()) {
lay::Action *a = item->action ();
if (a) {
a->set_shortcut (kb->second);
}
}
}
}

View File

@ -534,6 +534,16 @@ public:
*/
static std::vector<std::string> menu_symbols ();
/**
* @brief For internal use: apply current key bindings
*/
void apply_key_bindings ();
/**
* @brief For internal use: apply hidden menu flags
*/
void apply_hidden ();
/**
* @brief Open a new layout in mode 'mode'
*
@ -770,6 +780,7 @@ private:
double m_default_grid;
bool m_default_grids_updated;
std::vector<std::pair<std::string, std::string> > m_key_bindings;
std::vector<std::pair<std::string, bool> > m_hidden;
bool m_new_layout_current_panel;
bool m_synchronized_views;
bool m_synchronous;
@ -864,8 +875,6 @@ private:
void plugin_removed (lay::PluginDeclaration *cls);
void libraries_changed ();
void apply_key_bindings ();
void apply_hidden (const std::vector<std::pair <std::string, bool> > &hidden);
};
}

View File

@ -220,20 +220,22 @@ parse_menu_title (const std::string &s, std::string &title, std::string &shortcu
// AbstractMenuItem implementation
AbstractMenuItem::AbstractMenuItem (Dispatcher *dispatcher)
: mp_action (new Action ()), mp_dispatcher (dispatcher), m_has_submenu (false), m_remove_on_empty (false)
: mp_action (new Action ()), mp_dispatcher (dispatcher), m_has_submenu (false), m_remove_on_empty (false), m_primary (false)
{
// ... nothing yet ..
}
AbstractMenuItem::AbstractMenuItem (const AbstractMenuItem &item)
: mp_action (new Action ()), mp_dispatcher (item.dispatcher ()), m_has_submenu (false), m_remove_on_empty (false)
: mp_action (new Action ()), mp_dispatcher (item.dispatcher ()), m_has_submenu (false), m_remove_on_empty (false), m_primary (false)
{
// ... nothing yet ..
}
void
AbstractMenuItem::setup_item (const std::string &pn, const std::string &s, Action *a)
AbstractMenuItem::setup_item (const std::string &pn, const std::string &s, Action *a, bool primary)
{
m_primary = primary;
m_basename.clear ();
tl::Extractor ex (s.c_str ());
@ -1587,6 +1589,8 @@ AbstractMenu::items (const std::string &path) const
void
AbstractMenu::insert_item (const std::string &p, const std::string &name, Action *action)
{
bool primary = true;
tl::Extractor extr (p.c_str ());
while (! extr.at_end ()) {
@ -1601,7 +1605,8 @@ AbstractMenu::insert_item (const std::string &p, const std::string &name, Action
parent->children.insert (iter, AbstractMenuItem (mp_dispatcher));
--iter;
iter->setup_item (parent->name (), name, action);
iter->setup_item (parent->name (), name, action, primary);
primary = false;
// find any items with the same name and remove them
for (std::list<AbstractMenuItem>::iterator existing = parent->children.begin (); existing != parent->children.end (); ) {
@ -1635,7 +1640,7 @@ AbstractMenu::insert_separator (const std::string &p, const std::string &name)
--iter;
Action *action = new Action ();
action->set_separator (true);
iter->setup_item (parent->name (), name, action);
iter->setup_item (parent->name (), name, action, true);
}
@ -1655,7 +1660,7 @@ AbstractMenu::insert_menu (const std::string &p, const std::string &name, Action
parent->children.insert (iter, AbstractMenuItem (mp_dispatcher));
--iter;
iter->setup_item (parent->name (), name, action);
iter->setup_item (parent->name (), name, action, true);
iter->set_has_submenu ();
// find any items with the same name and remove them
@ -1945,7 +1950,7 @@ AbstractMenu::find_item (tl::Extractor &extr)
if (parent) {
parent->children.insert (iter, AbstractMenuItem (mp_dispatcher));
--iter;
iter->setup_item (parent->name (), n, new Action ());
iter->setup_item (parent->name (), n, new Action (), true);
iter->set_has_submenu ();
iter->set_remove_on_empty ();
iter->set_action_title (ndesc.empty () ? n : ndesc);
@ -2055,16 +2060,17 @@ AbstractMenu::get_shortcuts (const std::string &root, std::map<std::string, std:
std::vector<std::string> items = this->items (root);
for (std::vector<std::string>::const_iterator i = items.begin (); i != items.end (); ++i) {
if (i->size () > 0) {
if (is_valid (*i) && action (*i)->is_visible ()) {
if (is_menu (*i)) {
const AbstractMenuItem *item = find_item_exact (*i);
if (item && item->action () && item->action ()->is_visible ()) {
if (item->has_submenu ()) {
// a menu must be listed (so it can be hidden), but does not have a shortcut
// but we don't include special menus
if (i->at (0) != '@') {
bindings.insert (std::make_pair (*i, std::string ()));
}
get_shortcuts (*i, bindings, with_defaults);
} else if (! is_separator (*i)) {
bindings.insert (std::make_pair (*i, with_defaults ? action (*i)->get_default_shortcut () : action (*i)->get_effective_shortcut ()));
} else if (! item->action ()->is_separator () && item->primary ()) {
bindings.insert (std::make_pair (*i, with_defaults ? item->action ()->get_default_shortcut () : item->action ()->get_effective_shortcut ()));
}
}
}

View File

@ -559,7 +559,7 @@ struct LAYBASIC_PUBLIC AbstractMenuItem
/**
* @brief Internal method used to set up the item
*/
void setup_item (const std::string &pn, const std::string &n, Action *a);
void setup_item (const std::string &pn, const std::string &n, Action *a, bool primary);
Dispatcher *dispatcher () const
{
@ -616,6 +616,11 @@ struct LAYBASIC_PUBLIC AbstractMenuItem
return m_remove_on_empty;
}
bool primary () const
{
return m_primary;
}
std::list <AbstractMenuItem> children;
private:
@ -623,6 +628,7 @@ private:
Dispatcher *mp_dispatcher;
bool m_has_submenu;
bool m_remove_on_empty;
bool m_primary;
std::string m_name;
std::string m_basename;
std::set<std::string> m_groups;
@ -906,6 +912,16 @@ public:
return m_root;
}
/**
* @brief For internal use: gets the AbstractMenuItem for a given path or 0 if the path is not valid (const version)
*/
const AbstractMenuItem *find_item_exact (const std::string &path) const;
/**
* @brief For internal use: gets the AbstractMenuItem for a given path or 0 if the path is not valid
*/
AbstractMenuItem *find_item_exact (const std::string &path);
#if defined(HAVE_QT)
signals:
/**
@ -918,8 +934,6 @@ private:
friend class Action;
std::vector<std::pair<AbstractMenuItem *, std::list<AbstractMenuItem>::iterator> > find_item (tl::Extractor &extr);
const AbstractMenuItem *find_item_exact (const std::string &path) const;
AbstractMenuItem *find_item_exact (const std::string &path);
const AbstractMenuItem *find_item_for_action (const Action *action, const AbstractMenuItem *from = 0) const;
AbstractMenuItem *find_item_for_action (const Action *action, AbstractMenuItem *from = 0);
#if defined(HAVE_QT)

View File

@ -4084,7 +4084,9 @@ LayoutViewBase::cancel_edits ()
// the move service takes a special role here as it manages the
// transaction for the collective move operation.
mp_move_service->cancel ();
if (mp_move_service) {
mp_move_service->cancel ();
}
// cancel all drag and pending edit operations such as move operations.
mp_canvas->drag_cancel ();
@ -4099,7 +4101,9 @@ LayoutViewBase::finish_edits ()
{
// the move service takes a special role here as it manages the
// transaction for the collective move operation.
mp_move_service->finish ();
if (mp_move_service) {
mp_move_service->finish ();
}
// cancel all drag operations
mp_canvas->drag_cancel ();
@ -5460,7 +5464,7 @@ LayoutViewBase::paste_interactive (bool transient_mode)
// operations.
trans->close ();
if (mp_move_service->begin_move (trans.release (), transient_mode)) {
if (mp_move_service && mp_move_service->begin_move (trans.release (), transient_mode)) {
switch_mode (-1); // move mode
}
}

View File

@ -553,7 +553,7 @@ class Box:
"""
...
@overload
def enlarge(self, dx: int, dy: int) -> Box:
def enlarge(self, dx: Optional[int] = ..., dy: Optional[int] = ...) -> Box:
r"""
@brief Enlarges the box by a certain amount.
@ -596,7 +596,7 @@ class Box:
"""
...
@overload
def enlarged(self, dx: int, dy: int) -> Box:
def enlarged(self, dx: Optional[int] = ..., dy: Optional[int] = ...) -> Box:
r"""
@brief Enlarges the box by a certain amount.
@ -701,7 +701,7 @@ class Box:
"""
...
@overload
def moved(self, dx, 0: int, dy: Optional[int] = ...) -> Box:
def moved(self, dx: Optional[int] = ..., dy: Optional[int] = ...) -> Box:
r"""
@brief Moves the box by a certain distance
@ -7407,7 +7407,7 @@ class DBox:
"""
...
@overload
def enlarge(self, dx: float, dy: float) -> DBox:
def enlarge(self, dx: Optional[float] = ..., dy: Optional[float] = ...) -> DBox:
r"""
@brief Enlarges the box by a certain amount.
@ -7450,7 +7450,7 @@ class DBox:
"""
...
@overload
def enlarged(self, dx: float, dy: float) -> DBox:
def enlarged(self, dx: Optional[float] = ..., dy: Optional[float] = ...) -> DBox:
r"""
@brief Enlarges the box by a certain amount.
@ -7555,7 +7555,7 @@ class DBox:
"""
...
@overload
def moved(self, dx, 0: float, dy: Optional[float] = ...) -> DBox:
def moved(self, dx: Optional[float] = ..., dy: Optional[float] = ...) -> DBox:
r"""
@brief Moves the box by a certain distance
@ -30277,10 +30277,9 @@ class Instance:
@brief Gets the complex transformation of the instance or the first instance in the array
This method is always valid compared to \trans, since simple transformations can be expressed as complex transformations as well.
Setter:
@brief Sets the complex transformation of the instance or the first instance in the array (in micrometer units)
This method sets the transformation the same way as \cplx_trans=, but the displacement of this transformation is given in micrometer units. It is internally translated into database units.
@brief Sets the complex transformation of the instance or the first instance in the array
This method has been introduced in version 0.25.
This method has been introduced in version 0.23.
"""
da: DVector
r"""
@ -41368,15 +41367,15 @@ class NetPinRef:
@overload
def net(self) -> Net:
r"""
@brief Gets the net this pin reference is attached to (non-const version).
This constness variant has been introduced in version 0.26.8
@brief Gets the net this pin reference is attached to.
"""
...
@overload
def net(self) -> Net:
r"""
@brief Gets the net this pin reference is attached to.
@brief Gets the net this pin reference is attached to (non-const version).
This constness variant has been introduced in version 0.26.8
"""
...
def pin(self) -> Pin:
@ -41666,17 +41665,17 @@ class NetTerminalRef:
@overload
def device(self) -> Device:
r"""
@brief Gets the device reference.
@brief Gets the device reference (non-const version).
Gets the device object that this connection is made to.
This constness variant has been introduced in version 0.26.8
"""
...
@overload
def device(self) -> Device:
r"""
@brief Gets the device reference (non-const version).
@brief Gets the device reference.
Gets the device object that this connection is made to.
This constness variant has been introduced in version 0.26.8
"""
...
def device_class(self) -> DeviceClass:
@ -42686,15 +42685,6 @@ class Netlist:
"""
...
@overload
def circuit_by_name(self, name: str) -> Circuit:
r"""
@brief Gets the circuit object for a given name (const version).
If the name is not a valid circuit name, nil is returned.
This constness variant has been introduced in version 0.26.8.
"""
...
@overload
def circuit_by_name(self, name: str) -> Circuit:
r"""
@brief Gets the circuit object for a given name.
@ -42702,12 +42692,12 @@ class Netlist:
"""
...
@overload
def circuits_by_name(self, name_pattern: str) -> List[Circuit]:
def circuit_by_name(self, name: str) -> Circuit:
r"""
@brief Gets the circuit objects for a given name filter.
The name filter is a glob pattern. This method will return all \Circuit objects matching the glob pattern.
@brief Gets the circuit object for a given name (const version).
If the name is not a valid circuit name, nil is returned.
This method has been introduced in version 0.26.4.
This constness variant has been introduced in version 0.26.8.
"""
...
@overload
@ -42720,6 +42710,15 @@ class Netlist:
This constness variant has been introduced in version 0.26.8.
"""
...
@overload
def circuits_by_name(self, name_pattern: str) -> List[Circuit]:
r"""
@brief Gets the circuit objects for a given name filter.
The name filter is a glob pattern. This method will return all \Circuit objects matching the glob pattern.
This method has been introduced in version 0.26.4.
"""
...
def combine_devices(self) -> None:
r"""
@brief Combines devices where possible
@ -56566,10 +56565,11 @@ class Shape:
Starting with version 0.23, this method returns nil, if the shape does not represent a box.
Setter:
@brief Replaces the shape by the given box (in micrometer units)
This method replaces the shape by the given box, like \box= with a \Box argument does. This version translates the box from micrometer units to database units internally.
@brief Replaces the shape by the given box
This method replaces the shape by the given box. This method can only be called for editable layouts. It does not change the user properties of the shape.
Calling this method will invalidate any iterators. It should not be called inside a loop iterating over shapes.
This method has been introduced in version 0.25.
This method has been introduced in version 0.22.
"""
box_center: Point
r"""
@ -57238,10 +57238,10 @@ class Shape:
Applies to texts only. Will throw an exception if the object is not a text.
Setter:
@brief Sets the text transformation in micrometer units
@brief Sets the text transformation
Applies to texts only. Will throw an exception if the object is not a text.
This method has been introduced in version 0.25.
This method has been introduced in version 0.23.
"""
text_valign: int
r"""
@ -60854,16 +60854,16 @@ class SubCircuit(NetlistObject):
@overload
def circuit_ref(self) -> Circuit:
r"""
@brief Gets the circuit referenced by the subcircuit (non-const version).
This constness variant has been introduced in version 0.26.8
@brief Gets the circuit referenced by the subcircuit.
"""
...
@overload
def circuit_ref(self) -> Circuit:
r"""
@brief Gets the circuit referenced by the subcircuit.
@brief Gets the circuit referenced by the subcircuit (non-const version).
This constness variant has been introduced in version 0.26.8
"""
...
@overload
@ -61533,8 +61533,7 @@ class Text:
Setter:
@brief Sets the horizontal alignment
This property specifies how the text is aligned relative to the anchor point.
This property has been introduced in version 0.22 and extended to enums in 0.28.
This is the version accepting integer values. It's provided for backward compatibility.
"""
size: int
r"""
@ -61570,8 +61569,7 @@ class Text:
Setter:
@brief Sets the vertical alignment
This property specifies how the text is aligned relative to the anchor point.
This property has been introduced in version 0.22 and extended to enums in 0.28.
This is the version accepting integer values. It's provided for backward compatibility.
"""
x: int
r"""

View File

@ -271,6 +271,10 @@ class DBBox_TestClass < TestBase
assert_equal( a.to_s, "(-9,18;12,22)" )
a = b.moved( 1, -1 )
assert_equal( a.to_s, "(-9,16;12,20)" )
a = b.moved( dy: 1 )
assert_equal( a.to_s, "(-10,18;11,22)" )
a = b.moved( dx: 1 )
assert_equal( a.to_s, "(-9,17;12,21)" )
a = b.dup
a.move( 1, -1 )
@ -304,7 +308,12 @@ class DBBox_TestClass < TestBase
aa = a.dup
a.enlarge( -1, 1 )
assert_equal( a.to_s, "(-10,21;11,25)" )
a.enlarge( 1, -1 )
a.enlarge( dy: 1 )
a.enlarge( dx: -1 )
assert_equal( a.to_s, "(-10,21;11,25)" )
assert_equal( aa.enlarged( -1, 1 ).to_s, "(-10,21;11,25)" )
assert_equal( aa.enlarged( :dy => 1, :dx => -1 ).to_s, "(-10,21;11,25)" )
a = a.enlarged( RBA::Point::new(1, -1) )
assert_equal( a.to_s, "(-11,22;12,24)" )

View File

@ -2,10 +2,10 @@
# This script is sourced to define the main version parameters
# The main version
KLAYOUT_VERSION="0.29.9"
KLAYOUT_VERSION="0.29.10"
# The version used for PyPI (don't use variables here!)
KLAYOUT_PYPI_VERSION="0.29.9"
KLAYOUT_PYPI_VERSION="0.29.10"
# The build date
KLAYOUT_VERSION_DATE=$(date "+%Y-%m-%d")