"examples/cpp/vscode:/vscode.git/clone" did not exist on "fe46dac2c2ea1a988929fba05e9d3d3c9b11dfd7"
Commit 9499f642 authored by moto's avatar moto Committed by Facebook GitHub Bot
Browse files

Use module-level `__getattr__` to implement delayed initialization (#2377)

Summary:
This commit updates the lazy module initialization logic for
`torchaudio.prototype.io` and `torchaudio.prototype.ctc_decoder`.

- The modules are importable regarless of optional dependencies.
i.e. `import torchaudio.prototype.io` does not trigger the check for
optional dependencies.

- Optional dependencies are checked when the actual
API is imported for the first time.
i.e. `from torchaudio.prototype.io import Streamer` triggers the check
for optional dependencies.

The downside is that;

- `import torchaudio.prototype.io.Streamer` no longer works.

## Details:

Starting from Python 3.7, modules can bave `__getattr__` function,
which serves as a fallback if the import mechanism cannot find the
attribute.

This can be used to implement lazy import.

```python
def __getattr__(name):
    global pi
    if name == 'pi':
        import math
        pi = math.pi
        return pi
    raise AttributeError(...)
```

Ref: https://twitter.com/raymondh/status/1094686528440168453

The implementation performs lazy import for the APIs that work with
external/optional dependencies. In addition, it also check if the
binding is initialized only once.

## Why is this preferable approach?

Previously, the optional dependencies were checked at the tiem module
is imported;

https://github.com/pytorch/audio/blob/2f4eb4ac2f48a597825d3631a840afd855fe6b39/torchaudio/prototype/io/__init__.py#L1-L5

As long as this module is in `prototype`, which we ask users to import
explictly, users had control whether they want/do not want to install
the optional dependencies.

This approach only works for one optional dependencies per one module.
Say, we add different I/O library as an optional dependency, we need to
put all the APIs in dedicated submodule. This prevents us from having
flat namespace.
i.e. the I/O modules with multiple optional dependencies would look like

```python
# Client code
from torchaudio.io.foo import FooFeature
from torchaudio.io.bar import BarFeature
```

where the new approach would allow

```python
#client code
from torchaudio.io import FooFeature, BarFeature
```

Pull Request resolved: https://github.com/pytorch/audio/pull/2377

Reviewed By: nateanl

Differential Revision: D36305603

Pulled By: mthrok

fbshipit-source-id: c1eb6cac203f6dd0026d99f9a1de1af590a535ae
parent f5036c71
...@@ -8,12 +8,10 @@ import time ...@@ -8,12 +8,10 @@ import time
import unittest import unittest
import torch import torch
import torchaudio
from torch.testing._internal.common_utils import TestCase as PytorchTestCase from torch.testing._internal.common_utils import TestCase as PytorchTestCase
from torchaudio._internal.module_utils import is_module_available, is_sox_available, is_kaldi_available from torchaudio._internal.module_utils import is_module_available, is_sox_available, is_kaldi_available
from .backend_utils import set_audio_backend from .backend_utils import set_audio_backend
from .ctc_decoder_utils import is_ctc_decoder_available
class TempDirMixin: class TempDirMixin:
...@@ -109,13 +107,37 @@ class TorchaudioTestCase(TestBaseMixin, PytorchTestCase): ...@@ -109,13 +107,37 @@ class TorchaudioTestCase(TestBaseMixin, PytorchTestCase):
pass pass
_IS_FFMPEG_AVAILABLE = None
def is_ffmpeg_available(): def is_ffmpeg_available():
if _eval_env("TORCHAUDIO_TEST_IN_FBCODE", default=False): if _eval_env("TORCHAUDIO_TEST_IN_FBCODE", default=False):
return True return True
global _IS_FFMPEG_AVAILABLE
if _IS_FFMPEG_AVAILABLE is None:
try: try:
return torchaudio._extension._load_lib("libtorchaudio_ffmpeg") from torchaudio.prototype.io import Streamer # noqa: F401
_IS_FFMPEG_AVAILABLE = True
except Exception: except Exception:
return False _IS_FFMPEG_AVAILABLE = False
return _IS_FFMPEG_AVAILABLE
_IS_CTC_DECODER_AVAILABLE = None
def is_ctc_decoder_available():
global _IS_CTC_DECODER_AVAILABLE
if _IS_CTC_DECODER_AVAILABLE is None:
try:
from torchaudio.prototype.ctc_decoder import CTCDecoder # noqa: F401
_IS_CTC_DECODER_AVAILABLE = True
except Exception:
_IS_CTC_DECODER_AVAILABLE = False
return _IS_CTC_DECODER_AVAILABLE
def _eval_env(var, default): def _eval_env(var, default):
......
def is_ctc_decoder_available():
try:
import torchaudio.prototype.ctc_decoder # noqa: F401
return True
except ImportError:
return False
import torchaudio _INITIALIZED = False
_LAZILY_IMPORTED = [
try:
torchaudio._extension._load_lib("libtorchaudio_decoder")
from .ctc_decoder import Hypothesis, CTCDecoder, ctc_decoder, lexicon_decoder, download_pretrained_files
except ImportError as err:
raise ImportError(
"flashlight decoder bindings are required to use this functionality. "
"Please set BUILD_CTC_DECODER=1 when building from source."
) from err
__all__ = [
"Hypothesis", "Hypothesis",
"CTCDecoder", "CTCDecoder",
"ctc_decoder", "ctc_decoder",
"lexicon_decoder", "lexicon_decoder",
"download_pretrained_files", "download_pretrained_files",
] ]
def _init_extension():
import torchaudio
torchaudio._extension._load_lib("libtorchaudio_decoder")
global _INITIALIZED
_INITIALIZED = True
def __getattr__(name: str):
if name in _LAZILY_IMPORTED:
if not _INITIALIZED:
_init_extension()
try:
from . import _ctc_decoder
except AttributeError as err:
raise RuntimeError(
"CTC decoder requires the decoder extension. Please set BUILD_CTC_DECODER=1 when building from source."
) from err
item = getattr(_ctc_decoder, name)
globals()[name] = item
return item
raise AttributeError(f"module {__name__} has no attribute {name}")
def __dir__():
return sorted(__all__ + _LAZILY_IMPORTED)
__all__ = []
import torch _INITIALIZED = False
import torchaudio _LAZILY_IMPORTED = [
torchaudio._extension._load_lib("libtorchaudio_ffmpeg")
torch.ops.torchaudio.ffmpeg_init()
from .streamer import (
Streamer,
SourceStream,
SourceAudioStream,
SourceVideoStream,
OutputStream,
)
__all__ = [
"Streamer", "Streamer",
"SourceStream", "SourceStream",
"SourceAudioStream", "SourceAudioStream",
"SourceVideoStream", "SourceVideoStream",
"OutputStream", "OutputStream",
] ]
def _init_extension():
import torch
import torchaudio
try:
torchaudio._extension._load_lib("libtorchaudio_ffmpeg")
except OSError as err:
raise ImportError(
"Stream API requires FFmpeg libraries (libavformat and such). Please install FFmpeg 4."
) from err
try:
torch.ops.torchaudio.ffmpeg_init()
except RuntimeError as err:
raise RuntimeError(
"Stream API requires FFmpeg binding. Please set BUILD_FFMPEG=1 when building from source."
) from err
global _INITIALIZED
_INITIALIZED = True
def __getattr__(name: str):
if name in _LAZILY_IMPORTED:
if not _INITIALIZED:
_init_extension()
from . import streamer
item = getattr(streamer, name)
globals()[name] = item
return item
raise AttributeError(f"module {__name__} has no attribute {name}")
def __dir__():
return sorted(__all__ + _LAZILY_IMPORTED)
__all__ = []
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment