diff --git a/.meta/.readme.rst b/.meta/.readme.rst index 5f9e0f4f..12e46a61 100644 --- a/.meta/.readme.rst +++ b/.meta/.readme.rst @@ -918,7 +918,9 @@ For further customisation, (e.g. GUIs such as notebook or plotting packages). In the latter case: 1. ``def __init__()`` to call ``super().__init__(..., gui=True)`` to disable - terminal ``status_printer`` creation. + terminal ``status_printer`` creation. Otherwise (if terminal is required), + ``def __new__()`` to call ``cls.get_new()`` (see below) to ensure correct + nested positioning. 2. Redefine: ``close()``, ``clear()``, ``display()``. Consider overloading ``display()`` to use e.g. @@ -932,6 +934,23 @@ above recommendation: - `tqdm/contrib/telegram.py `__ - `tqdm/contrib/discord.py `__ +Note that multiple different ``tqdm`` subclasses which all write to the terminal +(``gui=False``) can cause positioning issues when used simultaneously (in nested +mode). To fix this, custom subclasses which expect to write to the terminal +should define a ``__new__()`` method as follows: + +.. code:: python + + from tqdm import tqdm as std_tqdm + + class TqdmExt(std_tqdm): + def __new__(cls, *args, **kwargs): + return cls.get_new(super(TqdmExt, cls), std_tqdm, *args, **kwargs) + +This approach is used ``tqdm.asyncio`` and ``tqdm.contrib.telegram/discord``. +However it is not necessary for ``tqdm.notebook/gui`` since they don't use the +terminal. + Dynamic Monitor/Meter ~~~~~~~~~~~~~~~~~~~~~ diff --git a/README.rst b/README.rst index 0284e60b..07eab656 100644 --- a/README.rst +++ b/README.rst @@ -1135,7 +1135,9 @@ For further customisation, (e.g. GUIs such as notebook or plotting packages). In the latter case: 1. ``def __init__()`` to call ``super().__init__(..., gui=True)`` to disable - terminal ``status_printer`` creation. + terminal ``status_printer`` creation. Otherwise (if terminal is required), + ``def __new__()`` to call ``cls.get_new()`` (see below) to ensure correct + nested positioning. 2. Redefine: ``close()``, ``clear()``, ``display()``. Consider overloading ``display()`` to use e.g. @@ -1149,6 +1151,23 @@ above recommendation: - `tqdm/contrib/telegram.py `__ - `tqdm/contrib/discord.py `__ +Note that multiple different ``tqdm`` subclasses which all write to the terminal +(``gui=False``) can cause positioning issues when used simultaneously (in nested +mode). To fix this, custom subclasses which expect to write to the terminal +should define a ``__new__()`` method as follows: + +.. code:: python + + from tqdm import tqdm as std_tqdm + + class TqdmExt(std_tqdm): + def __new__(cls, *args, **kwargs): + return cls.get_new(super(TqdmExt, cls), std_tqdm, *args, **kwargs) + +This approach is used ``tqdm.asyncio`` and ``tqdm.contrib.telegram/discord``. +However it is not necessary for ``tqdm.notebook/gui`` since they don't use the +terminal. + Dynamic Monitor/Meter ~~~~~~~~~~~~~~~~~~~~~ diff --git a/tqdm/asyncio.py b/tqdm/asyncio.py index e3e2d276..568fe0c8 100644 --- a/tqdm/asyncio.py +++ b/tqdm/asyncio.py @@ -63,24 +63,7 @@ class tqdm_asyncio(std_tqdm): total=total, **tqdm_kwargs) def __new__(cls, *args, **kwargs): - """ - Workaround for mixed-class same-stream nested progressbars. - See [#509](https://github.com/tqdm/tqdm/issues/509) - """ - with cls.get_lock(): - try: - cls._instances = std_tqdm._instances - except AttributeError: - pass - instance = super(tqdm_asyncio, cls).__new__(cls, *args, **kwargs) - with cls.get_lock(): - try: - # `std_tqdm` may have been changed so update - cls._instances.update(std_tqdm._instances) - except AttributeError: - pass - std_tqdm._instances = cls._instances - return instance + return cls.get_new(super(tqdm_asyncio, cls), std_tqdm, *args, **kwargs) def tarange(*args, **kwargs): diff --git a/tqdm/contrib/discord.py b/tqdm/contrib/discord.py index 9c0763aa..9212a595 100644 --- a/tqdm/contrib/discord.py +++ b/tqdm/contrib/discord.py @@ -103,24 +103,7 @@ class tqdm_discord(tqdm_auto): self.dio.write(self.format_meter(**fmt)) def __new__(cls, *args, **kwargs): - """ - Workaround for mixed-class same-stream nested progressbars. - See [#509](https://github.com/tqdm/tqdm/issues/509) - """ - with cls.get_lock(): - try: - cls._instances = tqdm_auto._instances - except AttributeError: - pass - instance = super(tqdm_discord, cls).__new__(cls, *args, **kwargs) - with cls.get_lock(): - try: - # `tqdm_auto` may have been changed so update - cls._instances.update(tqdm_auto._instances) - except AttributeError: - pass - tqdm_auto._instances = cls._instances - return instance + return cls.get_new(super(tqdm_discord, cls), tqdm_auto, *args, **kwargs) def tdrange(*args, **kwargs): diff --git a/tqdm/contrib/telegram.py b/tqdm/contrib/telegram.py index 1e1da6db..ccdbe5a5 100644 --- a/tqdm/contrib/telegram.py +++ b/tqdm/contrib/telegram.py @@ -107,24 +107,8 @@ class tqdm_telegram(tqdm_auto): self.tgio.write(self.format_meter(**fmt)) def __new__(cls, *args, **kwargs): - """ - Workaround for mixed-class same-stream nested progressbars. - See [#509](https://github.com/tqdm/tqdm/issues/509) - """ - with cls.get_lock(): - try: - cls._instances = tqdm_auto._instances - except AttributeError: - pass - instance = super(tqdm_telegram, cls).__new__(cls, *args, **kwargs) - with cls.get_lock(): - try: - # `tqdm_auto` may have been changed so update - cls._instances.update(tqdm_auto._instances) - except AttributeError: - pass - tqdm_auto._instances = cls._instances - return instance + return cls.get_new( + super(tqdm_telegram, cls), tqdm_auto, *args, **kwargs) def ttgrange(*args, **kwargs): diff --git a/tqdm/std.py b/tqdm/std.py index f525dea8..407e263a 100644 --- a/tqdm/std.py +++ b/tqdm/std.py @@ -575,6 +575,27 @@ class tqdm(Comparable): # Return the instance return instance + @classmethod + def get_new(cls, super_cls, base_cls, *args, **kwargs): + """ + Workaround for mixed-class same-stream nested progressbars. + See [#509](https://github.com/tqdm/tqdm/issues/509) + """ + with cls.get_lock(): + try: + cls._instances = base_cls._instances + except AttributeError: + pass + instance = super_cls.__new__(cls, *args, **kwargs) + with cls.get_lock(): + try: + # `base_cls` may have been changed so update + cls._instances.update(base_cls._instances) + except AttributeError: + pass + base_cls._instances = cls._instances + return instance + @classmethod def _get_free_pos(cls, instance=None): """Skips specified instance."""