Repository: abhinavsingh/proxy.py
Pull request #696 (2021-11-07 16:53:03)

Rename `Proxy.initialize` as `FlagParser.initialize`
Also move other `staticmethods` from within `Proxy` class into `utils`

codecov[bot]: # [Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) Report
> Merging [#696](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) (961c7b9) into [develop](https://codecov.io/gh/abhinavsingh/proxy.py/commit/f48771fb41a13fa31b2f02322cfbfd418f6f558b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) (f48771f) will **decrease** coverage by `0.07%`.
> The diff coverage is `95.69%`.

> :exclamation: Current head 961c7b9 differs from pull request most recent head 12762ad. Consider uploading reports for the commit 12762ad to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696/graphs/tree.svg?width=650&height=150&src=pr&token=Zh9J7b4la2&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)

```diff
@@             Coverage Diff             @@
##           develop     #696      +/-   ##
===========================================
- Coverage    86.26%   86.19%   -0.08%     
===========================================
  Files          108      108              
  Lines         4865     4868       +3     
  Branches       395      394       -1     
===========================================
- Hits          4197     4196       -1     
- Misses         596      598       +2     
- Partials        72       74       +2     
```

| Flag | Coverage Δ | |
|---|---|---|
| pytest | `86.13% <95.69%> (-0.02%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) | Coverage Δ | |
|---|---|---|
| [proxy/common/flag.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29tbW9uL2ZsYWcucHk=) | `94.44% <93.15%> (-5.56%)` | :arrow_down: |
| [proxy/common/utils.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvY29tbW9uL3V0aWxzLnB5) | `92.22% <94.02%> (+0.99%)` | :arrow_up: |
| [proxy/proxy.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-cHJveHkvcHJveHkucHk=) | `100.00% <100.00%> (+3.82%)` | :arrow_up: |
| [tests/common/test\_flags.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-dGVzdHMvY29tbW9uL3Rlc3RfZmxhZ3MucHk=) | `100.00% <100.00%> (ø)` | |
| [tests/core/test\_acceptor.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-dGVzdHMvY29yZS90ZXN0X2FjY2VwdG9yLnB5) | `100.00% <100.00%> (ø)` | |
| [tests/core/test\_acceptor\_pool.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-dGVzdHMvY29yZS90ZXN0X2FjY2VwdG9yX3Bvb2wucHk=) | `100.00% <100.00%> (ø)` | |
| [...sts/http/exceptions/test\_http\_proxy\_auth\_failed.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-dGVzdHMvaHR0cC9leGNlcHRpb25zL3Rlc3RfaHR0cF9wcm94eV9hdXRoX2ZhaWxlZC5weQ==) | `100.00% <100.00%> (ø)` | |
| [tests/http/test\_http\_proxy.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-dGVzdHMvaHR0cC90ZXN0X2h0dHBfcHJveHkucHk=) | `100.00% <100.00%> (ø)` | |
| [tests/http/test\_http\_proxy\_tls\_interception.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-dGVzdHMvaHR0cC90ZXN0X2h0dHBfcHJveHlfdGxzX2ludGVyY2VwdGlvbi5weQ==) | `99.02% <100.00%> (ø)` | |
| [tests/http/test\_protocol\_handler.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh#diff-dGVzdHMvaHR0cC90ZXN0X3Byb3RvY29sX2hhbmRsZXIucHk=) | `100.00% <100.00%> (ø)` | |
| ... and [8 more](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh). Last update [f48771f...12762ad](https://codecov.io/gh/abhinavsingh/proxy.py/pull/696?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Abhinav+Singh).


Patch:
diff --git a/Makefile b/Makefile
index bb404d7..adf28a9 100644
--- a/Makefile
+++ b/Makefile
@@ -18,6 +18,7 @@ CA_SIGNING_KEY_FILE_PATH := ca-signing-key.pem
 .PHONY: all https-certificates sign-https-certificates ca-certificates
 .PHONY: lib-version lib-clean lib-test lib-package lib-coverage lib-lint lib-pytest
 .PHONY: lib-release-test lib-release lib-profile
+.PHONY: lib-flake8, lib-mypy
 .PHONY: container container-run container-release
 .PHONY: devtools dashboard dashboard-clean
 
@@ -83,6 +84,12 @@ lib-clean:
 lib-lint:
 	python -m tox -e lint
 
+lib-flake8:
+	tox -e lint -- flake8 --all-files
+
+lib-mypy:
+	tox -e lint -- mypy --all-files
+
 lib-pytest:
 	python -m tox -e python -- -v
 
diff --git a/README.md b/README.md
index 048741e..fcf45ba 100644
--- a/README.md
+++ b/README.md
@@ -1550,6 +1550,12 @@ Dashboard is written in Typescript and SCSS, so let's build it first using:
 ❯ make dashboard
 ```
 
+Also build the embedded `Chrome DevTools` if you plan on using it:
+
+```console
+❯ make devtools
+```
+
 Now start `proxy.py` with dashboard plugin and by overriding root directory for static server:
 
 ```console
diff --git a/examples/https_connect_tunnel.py b/examples/https_connect_tunnel.py
index 6430724..f98b3e4 100644
--- a/examples/https_connect_tunnel.py
+++ b/examples/https_connect_tunnel.py
@@ -12,7 +12,7 @@ import time
 
 from typing import Any, Optional
 
-from proxy.proxy import Proxy
+from proxy.common.flag import FlagParser
 from proxy.common.utils import build_http_response
 from proxy.http.codes import httpStatusCodes
 from proxy.http.parser import httpParserStates
@@ -76,7 +76,9 @@ class HttpsConnectTunnelHandler(BaseTcpTunnelHandler):
 def main() -> None:
     # This example requires `threadless=True`
     with AcceptorPool(
-            flags=Proxy.initialize(port=12345, num_workers=1, threadless=True),
+            flags=FlagParser.initialize(
+                port=12345, num_workers=1, threadless=True,
+            ),
             work_klass=HttpsConnectTunnelHandler,
     ):
         try:
diff --git a/examples/ssl_echo_server.py b/examples/ssl_echo_server.py
index 7a7804f..b02f1f2 100644
--- a/examples/ssl_echo_server.py
+++ b/examples/ssl_echo_server.py
@@ -11,7 +11,7 @@
 import time
 from typing import Optional
 
-from proxy.proxy import Proxy
+from proxy.common.flag import FlagParser
 from proxy.common.utils import wrap_socket
 from proxy.core.acceptor import AcceptorPool
 from proxy.core.connection import TcpClientConnection
@@ -46,7 +46,7 @@ class EchoSSLServerHandler(BaseTcpServerHandler):
 def main() -> None:
     # This example requires `threadless=True`
     with AcceptorPool(
-        flags=Proxy.initialize(
+        flags=FlagParser.initialize(
             port=12345,
             num_workers=1,
             threadless=True,
diff --git a/examples/tcp_echo_server.py b/examples/tcp_echo_server.py
index 9e0e8f7..8453c95 100644
--- a/examples/tcp_echo_server.py
+++ b/examples/tcp_echo_server.py
@@ -11,7 +11,7 @@
 import time
 from typing import Optional
 
-from proxy.proxy import Proxy
+from proxy.common.flag import FlagParser
 from proxy.core.acceptor import AcceptorPool
 from proxy.core.base import BaseTcpServerHandler
 
@@ -31,7 +31,9 @@ class EchoServerHandler(BaseTcpServerHandler):
 def main() -> None:
     # This example requires `threadless=True`
     with AcceptorPool(
-        flags=Proxy.initialize(port=12345, num_workers=1, threadless=True),
+        flags=FlagParser.initialize(
+            port=12345, num_workers=1, threadless=True,
+        ),
         work_klass=EchoServerHandler,
     ):
         try:
diff --git a/examples/web_scraper.py b/examples/web_scraper.py
index 6d99da3..27f7953 100644
--- a/examples/web_scraper.py
+++ b/examples/web_scraper.py
@@ -13,7 +13,7 @@ import socket
 
 from typing import Dict
 
-from proxy.proxy import Proxy
+from proxy.common.flag import FlagParser
 from proxy.core.acceptor import Work, AcceptorPool
 from proxy.common.types import Readables, Writables
 
@@ -57,7 +57,7 @@ class WebScraper(Work):
 
 if __name__ == '__main__':
     with AcceptorPool(
-        flags=Proxy.initialize(
+        flags=FlagParser.initialize(
             port=12345,
             num_workers=1,
             threadless=True,
diff --git a/proxy/common/flag.py b/proxy/common/flag.py
index e23789d..ba35fcb 100644
--- a/proxy/common/flag.py
+++ b/proxy/common/flag.py
@@ -8,20 +8,45 @@
     :copyright: (c) 2013-present by Abhinav Singh and contributors.
     :license: BSD, see LICENSE for more details.
 """
+import os
+import sys
+import base64
+import socket
 import argparse
-from typing import Optional, List, Any
+import ipaddress
+import multiprocessing
+
+from typing import Optional, List, Any, cast
+
+from .types import IpAddress
+from .utils import text_, bytes_, setup_logger, is_py2, set_open_file_limit
+from .utils import get_default_plugins, import_plugin, load_plugins
+from .constants import COMMA, DEFAULT_DATA_DIRECTORY_PATH, DEFAULT_NUM_WORKERS
+from .constants import DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HEADERS, PY2_DEPRECATION_MESSAGE
 
 from .version import __version__
 
 __homepage__ = 'https://github.com/abhinavsingh/proxy.py'
 
 
+# TODO: Currently `initialize` staticmethod contains knowledge
+# about several common flags defined by proxy.py core.
+
+# This logic must be decoupled.  flags.add_argument must
+# also provide a callback to resolve the final flag value
+# based upon availability in input_args, **opts and
+# default values.
+
+# Supporting such a framework is complex but achievable.
+# One problem is that resolution of certain flags
+# can depend upon availability of other flags.
+
+# This will lead us into dependency graph modeling domain.
 class FlagParser:
     """Wrapper around argparse module.
 
-    proxy.py core and plugin classes must import `flag.flags` and
-    use `add_argument` to define their own flags within respective
-    class files.
+    Import `flag.flags` and use `add_argument` API
+    to define custom flags within respective Python files.
 
     Best Practice:
     1. Define flags at the top of your class files.
@@ -52,5 +77,206 @@ class FlagParser:
         self.args = self.parser.parse_args(input_args)
         return self.args
 
+    @staticmethod
+    def initialize(
+        input_args: Optional[List[str]]
+        = None, **opts: Any,
+    ) -> argparse.Namespace:
+        if input_args is None:
+            input_args = []
+
+        if is_py2():
+            print(PY2_DEPRECATION_MESSAGE)
+            sys.exit(1)
+
+        # Discover flags from requested plugin.
+        # This also surface external plugin flags under --help
+        for i, f in enumerate(input_args):
+            if f == '--plugin':
+                import_plugin(bytes_(input_args[i + 1]))
+
+        # Parse flags
+        args = flags.parse_args(input_args)
+
+        # Print version and exit
+        if args.version:
+            print(__version__)
+            sys.exit(0)
+
+        # Setup logging module
+        setup_logger(args.log_file, args.log_level, args.log_format)
+
+        # Setup limits
+        set_open_file_limit(args.open_file_limit)
+
+        # Load plugins
+        default_plugins = [bytes_(p) for p in get_default_plugins(args)]
+        extra_plugins = [
+            p if isinstance(p, type) else bytes_(p)
+            for p in opts.get('plugins', args.plugins.split(text_(COMMA)))
+            if not (isinstance(p, str) and len(p) == 0)
+        ]
+
+        # Load default plugins along with user provided --plugins
+        plugins = load_plugins(default_plugins + extra_plugins)
+
+        # proxy.py currently cannot serve over HTTPS and also perform TLS interception
+        # at the same time.  Check if user is trying to enable both feature
+        # at the same time.
+        if (args.cert_file and args.key_file) and \
+                (args.ca_key_file and args.ca_cert_file and args.ca_signing_key_file):
+            print(
+                'You can either enable end-to-end encryption OR TLS interception,'
+                'not both together.',
+            )
+            sys.exit(1)
+
+        # Generate auth_code required for basic authentication if enabled
+        auth_code = None
+        if args.basic_auth:
+            auth_code = base64.b64encode(bytes_(args.basic_auth))
+
+        # https://github.com/python/mypy/issues/5865
+        #
+        # def option(t: object, key: str, default: Any) -> Any:
+        #     return cast(t, opts.get(key, default))
+
+        args.plugins = plugins
+        args.auth_code = cast(
+            Optional[bytes],
+            opts.get(
+                'auth_code',
+                auth_code,
+            ),
+        )
+        args.server_recvbuf_size = cast(
+            int,
+            opts.get(
+                'server_recvbuf_size',
+                args.server_recvbuf_size,
+            ),
+        )
+        args.client_recvbuf_size = cast(
+            int,
+            opts.get(
+                'client_recvbuf_size',
+                args.client_recvbuf_size,
+            ),
+        )
+        args.pac_file = cast(
+            Optional[str], opts.get(
+                'pac_file', bytes_(
+                    args.pac_file,
+                ),
+            ),
+        )
+        args.pac_file_url_path = cast(
+            Optional[bytes], opts.get(
+                'pac_file_url_path', bytes_(
+                    args.pac_file_url_path,
+                ),
+            ),
+        )
+        disabled_headers = cast(
+            Optional[List[bytes]], opts.get(
+                'disable_headers', [
+                    header.lower()
+                    for header in bytes_(args.disable_headers).split(COMMA)
+                    if header.strip() != b''
+                ],
+            ),
+        )
+        args.disable_headers = disabled_headers if disabled_headers is not None else DEFAULT_DISABLE_HEADERS
+        args.certfile = cast(
+            Optional[str], opts.get(
+                'cert_file', args.cert_file,
+            ),
+        )
+        args.keyfile = cast(Optional[str], opts.get('key_file', args.key_file))
+        args.ca_key_file = cast(
+            Optional[str], opts.get(
+                'ca_key_file', args.ca_key_file,
+            ),
+        )
+        args.ca_cert_file = cast(
+            Optional[str], opts.get(
+                'ca_cert_file', args.ca_cert_file,
+            ),
+        )
+        args.ca_signing_key_file = cast(
+            Optional[str],
+            opts.get(
+                'ca_signing_key_file',
+                args.ca_signing_key_file,
+            ),
+        )
+        args.ca_file = cast(
+            Optional[str],
+            opts.get(
+                'ca_file',
+                args.ca_file,
+            ),
+        )
+        args.hostname = cast(
+            IpAddress,
+            opts.get('hostname', ipaddress.ip_address(args.hostname)),
+        )
+        args.family = socket.AF_INET6 if args.hostname.version == 6 else socket.AF_INET
+        args.port = cast(int, opts.get('port', args.port))
+        args.backlog = cast(int, opts.get('backlog', args.backlog))
+        num_workers = opts.get('num_workers', args.num_workers)
+        num_workers = num_workers if num_workers is not None else DEFAULT_NUM_WORKERS
+        args.num_workers = cast(
+            int, num_workers if num_workers > 0 else multiprocessing.cpu_count(),
+        )
+        args.static_server_dir = cast(
+            str,
+            opts.get(
+                'static_server_dir',
+                args.static_server_dir,
+            ),
+        )
+        args.enable_static_server = cast(
+            bool,
+            opts.get(
+                'enable_static_server',
+                args.enable_static_server,
+            ),
+        )
+        args.devtools_ws_path = cast(
+            bytes,
+            opts.get(
+                'devtools_ws_path',
+                getattr(args, 'devtools_ws_path', DEFAULT_DEVTOOLS_WS_PATH),
+            ),
+        )
+        args.timeout = cast(int, opts.get('timeout', args.timeout))
+        args.threadless = cast(bool, opts.get('threadless', args.threadless))
+        args.enable_events = cast(
+            bool,
+            opts.get(
+                'enable_events',
+                args.enable_events,
+            ),
+        )
+        args.pid_file = cast(
+            Optional[str], opts.get(
+                'pid_file', args.pid_file,
+            ),
+        )
+
+        args.proxy_py_data_dir = DEFAULT_DATA_DIRECTORY_PATH
+        os.makedirs(args.proxy_py_data_dir, exist_ok=True)
+
+        ca_cert_dir = opts.get('ca_cert_dir', args.ca_cert_dir)
+        args.ca_cert_dir = cast(Optional[str], ca_cert_dir)
+        if args.ca_cert_dir is None:
+            args.ca_cert_dir = os.path.join(
+                args.proxy_py_data_dir, 'certificates',
+            )
+            os.makedirs(args.ca_cert_dir, exist_ok=True)
+
+        return args
+
 
 flags = FlagParser()
diff --git a/proxy/common/utils.py b/proxy/common/utils.py
index a325226..9e92888 100644
--- a/proxy/common/utils.py
+++ b/proxy/common/utils.py
@@ -8,18 +8,38 @@
     :copyright: (c) 2013-present by Abhinav Singh and contributors.
     :license: BSD, see LICENSE for more details.
 """
+import os
+import abc
+import sys
 import ssl
 import socket
 import logging
+import inspect
+import argparse
+import importlib
 import functools
 import ipaddress
 import contextlib
+import collections
 
 from types import TracebackType
-from typing import Optional, Dict, Any, List, Tuple, Type, Callable
+from typing import Optional, Dict, Any, List, Tuple, Type, Callable, Union
 
 from .constants import HTTP_1_1, COLON, WHITESPACE, CRLF, DEFAULT_TIMEOUT
 from .constants import DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT, DEFAULT_LOG_LEVEL
+from .constants import DOT, PLUGIN_DASHBOARD, PLUGIN_DEVTOOLS_PROTOCOL
+from .constants import PLUGIN_HTTP_PROXY, PLUGIN_INSPECT_TRAFFIC, PLUGIN_PAC_FILE
+from .constants import PLUGIN_WEB_SERVER, PLUGIN_PROXY_AUTH
+
+if os.name != 'nt':
+    import resource
+
+logger = logging.getLogger(__name__)
+
+
+def is_py2() -> bool:
+    """Exists only to avoid mocking sys.version_info in tests."""
+    return sys.version_info[0] == 2
 
 
 def text_(s: Any, encoding: str = 'utf-8', errors: str = 'strict') -> Any:
@@ -271,3 +291,93 @@ def setup_logger(
         )
     else:
         logging.basicConfig(level=ll, format=log_format)
+
+
+def load_plugins(
+    plugins: List[Union[bytes, type]],
+) -> Dict[bytes, List[type]]:
+    """Accepts a comma separated list of Python modules and returns
+    a list of respective Python classes."""
+    p: Dict[bytes, List[type]] = {
+        b'HttpProtocolHandlerPlugin': [],
+        b'HttpProxyBasePlugin': [],
+        b'HttpWebServerBasePlugin': [],
+        b'ProxyDashboardWebsocketPlugin': [],
+    }
+    for plugin_ in plugins:
+        klass, module_name = import_plugin(plugin_)
+        assert klass and module_name
+        mro = list(inspect.getmro(klass))
+        mro.reverse()
+        iterator = iter(mro)
+        while next(iterator) is not abc.ABC:
+            pass
+        base_klass = next(iterator)
+        if klass not in p[bytes_(base_klass.__name__)]:
+            p[bytes_(base_klass.__name__)].append(klass)
+        logger.info('Loaded plugin %s.%s', module_name, klass.__name__)
+    return p
+
+
+def import_plugin(plugin: Union[bytes, type]) -> Any:
+    if isinstance(plugin, type):
+        module_name = '__main__'
+        klass = plugin
+    else:
+        plugin_ = text_(plugin.strip())
+        assert plugin_ != ''
+        module_name, klass_name = plugin_.rsplit(text_(DOT), 1)
+        klass = getattr(
+            importlib.import_module(
+                module_name.replace(
+                    os.path.sep, text_(DOT),
+                ),
+            ),
+            klass_name,
+        )
+    return (klass, module_name)
+
+
+def get_default_plugins(
+        args: argparse.Namespace,
+) -> List[str]:
+    """Prepare list of plugins to load based upon
+    --enable-*, --disable-* and --basic-auth flags.
+    """
+    default_plugins: List[str] = []
+    if args.basic_auth is not None:
+        default_plugins.append(PLUGIN_PROXY_AUTH)
+    if args.enable_dashboard:
+        default_plugins.append(PLUGIN_WEB_SERVER)
+        args.enable_static_server = True
+        default_plugins.append(PLUGIN_DASHBOARD)
+        default_plugins.append(PLUGIN_INSPECT_TRAFFIC)
+        args.enable_events = True
+        args.enable_devtools = True
+    if args.enable_devtools:
+        default_plugins.append(PLUGIN_DEVTOOLS_PROTOCOL)
+        default_plugins.append(PLUGIN_WEB_SERVER)
+    if not args.disable_http_proxy:
+        default_plugins.append(PLUGIN_HTTP_PROXY)
+    if args.enable_web_server or \
+            args.pac_file is not None or \
+            args.enable_static_server:
+        default_plugins.append(PLUGIN_WEB_SERVER)
+    if args.pac_file is not None:
+        default_plugins.append(PLUGIN_PAC_FILE)
+    return list(collections.OrderedDict.fromkeys(default_plugins).keys())
+
+
+def set_open_file_limit(soft_limit: int) -> None:
+    """Configure open file description soft limit on supported OS."""
+    if os.name != 'nt':  # resource module not available on Windows OS
+        curr_soft_limit, curr_hard_limit = resource.getrlimit(
+            resource.RLIMIT_NOFILE,
+        )
+        if curr_soft_limit < soft_limit < curr_hard_limit:
+            resource.setrlimit(
+                resource.RLIMIT_NOFILE, (soft_limit, curr_hard_limit),
+            )
+            logger.debug(
+                'Open file soft limit set to %d', soft_limit,
+            )
diff --git a/proxy/proxy.py b/proxy/proxy.py
index 6f9d207..4ccc290 100644
--- a/proxy/proxy.py
+++ b/proxy/proxy.py
@@ -8,45 +8,28 @@
     :copyright: (c) 2013-present by Abhinav Singh and contributors.
     :license: BSD, see LICENSE for more details.
 """
-import abc
-import argparse
-import base64
-import collections
-import ipaddress
-import multiprocessing
 import os
-import socket
 import sys
 import time
 import logging
-import importlib
-import inspect
 
 from types import TracebackType
-from typing import Dict, List, Optional, Any, Type, Union, cast
+from typing import List, Optional, Any, Type
 
 from proxy.core.acceptor.work import Work
 
-from .common.utils import bytes_, text_, setup_logger
-from .common.types import IpAddress
-from .common.version import __version__
+from .common.utils import bytes_
 from .core.acceptor import AcceptorPool
 from .http.handler import HttpProtocolHandler
 from .core.event import EventManager
-from .common.flag import flags
-from .common.constants import COMMA, DEFAULT_DATA_DIRECTORY_PATH, PLUGIN_PROXY_AUTH
-from .common.constants import DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HEADERS
-from .common.constants import DEFAULT_DISABLE_HTTP_PROXY, DEFAULT_NUM_WORKERS
+from .common.flag import FlagParser, flags
+from .common.constants import DEFAULT_DISABLE_HTTP_PROXY, DEFAULT_ENABLE_EVENTS
 from .common.constants import DEFAULT_ENABLE_DASHBOARD, DEFAULT_ENABLE_DEVTOOLS
 from .common.constants import DEFAULT_ENABLE_STATIC_SERVER, DEFAULT_ENABLE_WEB_SERVER
 from .common.constants import DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT, DEFAULT_LOG_LEVEL
 from .common.constants import DEFAULT_OPEN_FILE_LIMIT, DEFAULT_PID_FILE, DEFAULT_PLUGINS
-from .common.constants import DEFAULT_VERSION, DOT, PLUGIN_DASHBOARD, PLUGIN_DEVTOOLS_PROTOCOL
-from .common.constants import PLUGIN_HTTP_PROXY, PLUGIN_INSPECT_TRAFFIC, PLUGIN_PAC_FILE
-from .common.constants import PLUGIN_WEB_SERVER, PY2_DEPRECATION_MESSAGE, DEFAULT_ENABLE_EVENTS
+from .common.constants import DEFAULT_VERSION
 
-if os.name != 'nt':
-    import resource
 
 logger = logging.getLogger(__name__)
 
@@ -151,7 +134,7 @@ class Proxy:
     """
 
     def __init__(self, input_args: Optional[List[str]], **opts: Any) -> None:
-        self.flags = Proxy.initialize(input_args, **opts)
+        self.flags = FlagParser.initialize(input_args, **opts)
         self.pool: Optional[AcceptorPool] = None
         # TODO(abhinavsingh): Allow users to override the worker class itself
         # e.g. A clear text protocol. Or imagine a TelnetProtocolHandler instead
@@ -197,302 +180,6 @@ class Proxy:
             self.event_manager.stop_event_dispatcher()
         self.delete_pid_file()
 
-    @staticmethod
-    def initialize(
-        input_args: Optional[List[str]]
-        = None, **opts: Any,
-    ) -> argparse.Namespace:
-        if input_args is None:
-            input_args = []
-
-        if Proxy.is_py2():
-            print(PY2_DEPRECATION_MESSAGE)
-            sys.exit(1)
-
-        # Discover flags from requested plugin.
-        # This also surface external plugin flags under --help
-        for i, f in enumerate(input_args):
-            if f == '--plugin':
-                Proxy.import_plugin(bytes_(input_args[i + 1]))
-
-        # Parse flags
-        args = flags.parse_args(input_args)
-
-        # Print version and exit
-        if args.version:
-            print(__version__)
-            sys.exit(0)
-
-        # Setup logging module
-        setup_logger(args.log_file, args.log_level, args.log_format)
-
-        # Setup limits
-        Proxy.set_open_file_limit(args.open_file_limit)
-
-        # Load plugins
-        default_plugins = [bytes_(p) for p in Proxy.get_default_plugins(args)]
-        extra_plugins = [
-            p if isinstance(p, type) else bytes_(p)
-            for p in opts.get('plugins', args.plugins.split(text_(COMMA)))
-            if not (isinstance(p, str) and len(p) == 0)
-        ]
-
-        # Load default plugins along with user provided --plugins
-        plugins = Proxy.load_plugins(default_plugins + extra_plugins)
-
-        # proxy.py currently cannot serve over HTTPS and also perform TLS interception
-        # at the same time.  Check if user is trying to enable both feature
-        # at the same time.
-        if (args.cert_file and args.key_file) and \
-                (args.ca_key_file and args.ca_cert_file and args.ca_signing_key_file):
-            print(
-                'You can either enable end-to-end encryption OR TLS interception,'
-                'not both together.',
-            )
-            sys.exit(1)
-
-        # Generate auth_code required for basic authentication if enabled
-        auth_code = None
-        if args.basic_auth:
-            auth_code = base64.b64encode(bytes_(args.basic_auth))
-
-        # https://github.com/python/mypy/issues/5865
-        #
-        # def option(t: object, key: str, default: Any) -> Any:
-        #     return cast(t, opts.get(key, default))
-
-        args.plugins = plugins
-        args.auth_code = cast(
-            Optional[bytes],
-            opts.get(
-                'auth_code',
-                auth_code,
-            ),
-        )
-        args.server_recvbuf_size = cast(
-            int,
-            opts.get(
-                'server_recvbuf_size',
-                args.server_recvbuf_size,
-            ),
-        )
-        args.client_recvbuf_size = cast(
-            int,
-            opts.get(
-                'client_recvbuf_size',
-                args.client_recvbuf_size,
-            ),
-        )
-        args.pac_file = cast(
-            Optional[str], opts.get(
-                'pac_file', bytes_(
-                    args.pac_file,
-                ),
-            ),
-        )
-        args.pac_file_url_path = cast(
-            Optional[bytes], opts.get(
-                'pac_file_url_path', bytes_(
-                    args.pac_file_url_path,
-                ),
-            ),
-        )
-        disabled_headers = cast(
-            Optional[List[bytes]], opts.get(
-                'disable_headers', [
-                    header.lower()
-                    for header in bytes_(args.disable_headers).split(COMMA)
-                    if header.strip() != b''
-                ],
-            ),
-        )
-        args.disable_headers = disabled_headers if disabled_headers is not None else DEFAULT_DISABLE_HEADERS
-        args.certfile = cast(
-            Optional[str], opts.get(
-                'cert_file', args.cert_file,
-            ),
-        )
-        args.keyfile = cast(Optional[str], opts.get('key_file', args.key_file))
-        args.ca_key_file = cast(
-            Optional[str], opts.get(
-                'ca_key_file', args.ca_key_file,
-            ),
-        )
-        args.ca_cert_file = cast(
-            Optional[str], opts.get(
-                'ca_cert_file', args.ca_cert_file,
-            ),
-        )
-        args.ca_signing_key_file = cast(
-            Optional[str],
-            opts.get(
-                'ca_signing_key_file',
-                args.ca_signing_key_file,
-            ),
-        )
-        args.ca_file = cast(
-            Optional[str],
-            opts.get(
-                'ca_file',
-                args.ca_file,
-            ),
-        )
-        args.hostname = cast(
-            IpAddress,
-            opts.get('hostname', ipaddress.ip_address(args.hostname)),
-        )
-        args.family = socket.AF_INET6 if args.hostname.version == 6 else socket.AF_INET
-        args.port = cast(int, opts.get('port', args.port))
-        args.backlog = cast(int, opts.get('backlog', args.backlog))
-        num_workers = opts.get('num_workers', args.num_workers)
-        num_workers = num_workers if num_workers is not None else DEFAULT_NUM_WORKERS
-        args.num_workers = cast(
-            int, num_workers if num_workers > 0 else multiprocessing.cpu_count(),
-        )
-        args.static_server_dir = cast(
-            str,
-            opts.get(
-                'static_server_dir',
-                args.static_server_dir,
-            ),
-        )
-        args.enable_static_server = cast(
-            bool,
-            opts.get(
-                'enable_static_server',
-                args.enable_static_server,
-            ),
-        )
-        args.devtools_ws_path = cast(
-            bytes,
-            opts.get(
-                'devtools_ws_path',
-                getattr(args, 'devtools_ws_path', DEFAULT_DEVTOOLS_WS_PATH),
-            ),
-        )
-        args.timeout = cast(int, opts.get('timeout', args.timeout))
-        args.threadless = cast(bool, opts.get('threadless', args.threadless))
-        args.enable_events = cast(
-            bool,
-            opts.get(
-                'enable_events',
-                args.enable_events,
-            ),
-        )
-        args.pid_file = cast(
-            Optional[str], opts.get(
-                'pid_file', args.pid_file,
-            ),
-        )
-
-        args.proxy_py_data_dir = DEFAULT_DATA_DIRECTORY_PATH
-        os.makedirs(args.proxy_py_data_dir, exist_ok=True)
-
-        ca_cert_dir = opts.get('ca_cert_dir', args.ca_cert_dir)
-        args.ca_cert_dir = cast(Optional[str], ca_cert_dir)
-        if args.ca_cert_dir is None:
-            args.ca_cert_dir = os.path.join(
-                args.proxy_py_data_dir, 'certificates',
-            )
-            os.makedirs(args.ca_cert_dir, exist_ok=True)
-
-        return args
-
-    @staticmethod
-    def load_plugins(
-        plugins: List[Union[bytes, type]],
-    ) -> Dict[bytes, List[type]]:
-        """Accepts a comma separated list of Python modules and returns
-        a list of respective Python classes."""
-        p: Dict[bytes, List[type]] = {
-            b'HttpProtocolHandlerPlugin': [],
-            b'HttpProxyBasePlugin': [],
-            b'HttpWebServerBasePlugin': [],
-            b'ProxyDashboardWebsocketPlugin': [],
-        }
-        for plugin_ in plugins:
-            klass, module_name = Proxy.import_plugin(plugin_)
-            assert klass and module_name
-            mro = list(inspect.getmro(klass))
-            mro.reverse()
-            iterator = iter(mro)
-            while next(iterator) is not abc.ABC:
-                pass
-            base_klass = next(iterator)
-            if klass not in p[bytes_(base_klass.__name__)]:
-                p[bytes_(base_klass.__name__)].append(klass)
-            logger.info('Loaded plugin %s.%s', module_name, klass.__name__)
-        return p
-
-    @staticmethod
-    def import_plugin(plugin: Union[bytes, type]) -> Any:
-        if isinstance(plugin, type):
-            module_name = '__main__'
-            klass = plugin
-        else:
-            plugin_ = text_(plugin.strip())
-            assert plugin_ != ''
-            module_name, klass_name = plugin_.rsplit(text_(DOT), 1)
-            klass = getattr(
-                importlib.import_module(
-                    module_name.replace(
-                        os.path.sep, text_(DOT),
-                    ),
-                ),
-                klass_name,
-            )
-        return (klass, module_name)
-
-    @staticmethod
-    def get_default_plugins(
-            args: argparse.Namespace,
-    ) -> List[str]:
-        """Prepare list of plugins to load based upon
-        --enable-*, --disable-* and --basic-auth flags.
-        """
-        default_plugins: List[str] = []
-        if args.basic_auth is not None:
-            default_plugins.append(PLUGIN_PROXY_AUTH)
-        if args.enable_dashboard:
-            default_plugins.append(PLUGIN_WEB_SERVER)
-            args.enable_static_server = True
-            default_plugins.append(PLUGIN_DASHBOARD)
-            default_plugins.append(PLUGIN_INSPECT_TRAFFIC)
-            args.enable_events = True
-            args.enable_devtools = True
-        if args.enable_devtools:
-            default_plugins.append(PLUGIN_DEVTOOLS_PROTOCOL)
-            default_plugins.append(PLUGIN_WEB_SERVER)
-        if not args.disable_http_proxy:
-            default_plugins.append(PLUGIN_HTTP_PROXY)
-        if args.enable_web_server or \
-                args.pac_file is not None or \
-                args.enable_static_server:
-            default_plugins.append(PLUGIN_WEB_SERVER)
-        if args.pac_file is not None:
-            default_plugins.append(PLUGIN_PAC_FILE)
-        return list(collections.OrderedDict.fromkeys(default_plugins).keys())
-
-    @staticmethod
-    def is_py2() -> bool:
-        """Exists only to avoid mocking sys.version_info in tests."""
-        return sys.version_info[0] == 2
-
-    @staticmethod
-    def set_open_file_limit(soft_limit: int) -> None:
-        """Configure open file description soft limit on supported OS."""
-        if os.name != 'nt':  # resource module not available on Windows OS
-            curr_soft_limit, curr_hard_limit = resource.getrlimit(
-                resource.RLIMIT_NOFILE,
-            )
-            if curr_soft_limit < soft_limit < curr_hard_limit:
-                resource.setrlimit(
-                    resource.RLIMIT_NOFILE, (soft_limit, curr_hard_limit),
-                )
-                logger.debug(
-                    'Open file soft limit set to %d', soft_limit,
-                )
-
 
 def main(
         input_args: Optional[List[str]] = None,


Test patch:
diff --git a/tests/common/test_flags.py b/tests/common/test_flags.py
index a11c9ec..25c8212 100644
--- a/tests/common/test_flags.py
+++ b/tests/common/test_flags.py
@@ -14,7 +14,7 @@ import unittest
 
 from typing import List, Dict
 
-from proxy.proxy import Proxy
+from proxy.common.flag import FlagParser
 from proxy.http.proxy import HttpProxyPlugin
 from proxy.plugin import CacheResponsesPlugin
 from proxy.plugin import FilterByUpstreamHostPlugin
@@ -31,7 +31,7 @@ class TestFlags(unittest.TestCase):
                 )
 
     def test_load_plugin_from_bytes(self) -> None:
-        self.flags = Proxy.initialize(
+        self.flags = FlagParser.initialize(
             [], plugins=[
                 b'proxy.plugin.CacheResponsesPlugin',
             ],
@@ -39,7 +39,7 @@ class TestFlags(unittest.TestCase):
         self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]})
 
     def test_load_plugins_from_bytes(self) -> None:
-        self.flags = Proxy.initialize(
+        self.flags = FlagParser.initialize(
             [], plugins=[
                 b'proxy.plugin.CacheResponsesPlugin',
                 b'proxy.plugin.FilterByUpstreamHostPlugin',
@@ -53,13 +53,13 @@ class TestFlags(unittest.TestCase):
         })
 
     def test_load_plugin_from_args(self) -> None:
-        self.flags = Proxy.initialize([
+        self.flags = FlagParser.initialize([
             '--plugins', 'proxy.plugin.CacheResponsesPlugin',
         ])
         self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]})
 
     def test_load_plugins_from_args(self) -> None:
-        self.flags = Proxy.initialize([
+        self.flags = FlagParser.initialize([
             '--plugins', 'proxy.plugin.CacheResponsesPlugin,proxy.plugin.FilterByUpstreamHostPlugin',
         ])
         self.assert_plugins({
@@ -70,7 +70,7 @@ class TestFlags(unittest.TestCase):
         })
 
     def test_load_plugin_from_class(self) -> None:
-        self.flags = Proxy.initialize(
+        self.flags = FlagParser.initialize(
             [], plugins=[
                 CacheResponsesPlugin,
             ],
@@ -78,7 +78,7 @@ class TestFlags(unittest.TestCase):
         self.assert_plugins({'HttpProxyBasePlugin': [CacheResponsesPlugin]})
 
     def test_load_plugins_from_class(self) -> None:
-        self.flags = Proxy.initialize(
+        self.flags = FlagParser.initialize(
             [], plugins=[
                 CacheResponsesPlugin,
                 FilterByUpstreamHostPlugin,
@@ -92,7 +92,7 @@ class TestFlags(unittest.TestCase):
         })
 
     def test_load_plugins_from_bytes_and_class(self) -> None:
-        self.flags = Proxy.initialize(
+        self.flags = FlagParser.initialize(
             [], plugins=[
                 CacheResponsesPlugin,
                 b'proxy.plugin.FilterByUpstreamHostPlugin',
@@ -106,7 +106,7 @@ class TestFlags(unittest.TestCase):
         })
 
     def test_unique_plugin_from_bytes(self) -> None:
-        self.flags = Proxy.initialize(
+        self.flags = FlagParser.initialize(
             [], plugins=[
                 bytes_(PLUGIN_HTTP_PROXY),
             ],
@@ -118,7 +118,7 @@ class TestFlags(unittest.TestCase):
         })
 
     def test_unique_plugin_from_args(self) -> None:
-        self.flags = Proxy.initialize([
+        self.flags = FlagParser.initialize([
             '--plugins', PLUGIN_HTTP_PROXY,
         ])
         self.assert_plugins({
@@ -128,7 +128,7 @@ class TestFlags(unittest.TestCase):
         })
 
     def test_unique_plugin_from_class(self) -> None:
-        self.flags = Proxy.initialize(
+        self.flags = FlagParser.initialize(
             [], plugins=[
                 HttpProxyPlugin,
             ],
diff --git a/tests/core/test_acceptor.py b/tests/core/test_acceptor.py
index ceb863d..5eddcab 100644
--- a/tests/core/test_acceptor.py
+++ b/tests/core/test_acceptor.py
@@ -15,7 +15,7 @@ import multiprocessing
 from unittest import mock
 
 from proxy.core.acceptor import Acceptor
-from proxy.proxy import Proxy
+from proxy.common.flag import FlagParser
 
 
 class TestAcceptor(unittest.TestCase):
@@ -24,7 +24,7 @@ class TestAcceptor(unittest.TestCase):
         self.acceptor_id = 1
         self.mock_protocol_handler = mock.MagicMock()
         self.pipe = multiprocessing.Pipe()
-        self.flags = Proxy.initialize()
+        self.flags = FlagParser.initialize()
         self.acceptor = Acceptor(
             idd=self.acceptor_id,
             work_queue=self.pipe[1],
diff --git a/tests/core/test_acceptor_pool.py b/tests/core/test_acceptor_pool.py
index c7f46bf..d84d3b2 100644
--- a/tests/core/test_acceptor_pool.py
+++ b/tests/core/test_acceptor_pool.py
@@ -12,7 +12,7 @@ import unittest
 import socket
 from unittest import mock
 
-from proxy.proxy import Proxy
+from proxy.common.flag import FlagParser
 from proxy.core.acceptor import AcceptorPool
 
 
@@ -36,7 +36,7 @@ class TestAcceptorPool(unittest.TestCase):
         num_workers = 2
         sock = mock_socket.return_value
         work_klass = mock.MagicMock()
-        flags = Proxy.initialize(num_workers=2)
+        flags = FlagParser.initialize(num_workers=2)
 
         pool = AcceptorPool(flags=flags, work_klass=work_klass)
         pool.setup()
diff --git a/tests/http/exceptions/test_http_proxy_auth_failed.py b/tests/http/exceptions/test_http_proxy_auth_failed.py
index 312106f..82691b7 100644
--- a/tests/http/exceptions/test_http_proxy_auth_failed.py
+++ b/tests/http/exceptions/test_http_proxy_auth_failed.py
@@ -12,7 +12,7 @@ import selectors
 import unittest
 from unittest import mock
 
-from proxy.proxy import Proxy
+from proxy.common.flag import FlagParser
 from proxy.http.exception.proxy_auth_failed import ProxyAuthenticationFailed
 from proxy.http.handler import HttpProtocolHandler
 from proxy.core.connection import TcpClientConnection
@@ -33,7 +33,7 @@ class TestHttpProxyAuthFailed(unittest.TestCase):
 
         self.fileno = 10
         self._addr = ('127.0.0.1', 54382)
-        self.flags = Proxy.initialize(["--basic-auth", "user:pass"])
+        self.flags = FlagParser.initialize(["--basic-auth", "user:pass"])
         self._conn = mock_fromfd.return_value
         self.protocol_handler = HttpProtocolHandler(
             TcpClientConnection(self._conn, self._addr),
diff --git a/tests/http/test_http_proxy.py b/tests/http/test_http_proxy.py
index e43bacd..0b27eaa 100644
--- a/tests/http/test_http_proxy.py
+++ b/tests/http/test_http_proxy.py
@@ -13,7 +13,7 @@ import selectors
 from unittest import mock
 
 from proxy.common.constants import DEFAULT_HTTP_PORT
-from proxy.proxy import Proxy
+from proxy.common.flag import FlagParser
 from proxy.core.connection import TcpClientConnection
 from proxy.http.proxy import HttpProxyPlugin
 from proxy.http.handler import HttpProtocolHandler
@@ -35,7 +35,7 @@ class TestHttpProxyPlugin(unittest.TestCase):
 
         self.fileno = 10
         self._addr = ('127.0.0.1', 54382)
-        self.flags = Proxy.initialize()
+        self.flags = FlagParser.initialize()
         self.plugin = mock.MagicMock()
         self.flags.plugins = {
             b'HttpProtocolHandlerPlugin': [HttpProxyPlugin],
diff --git a/tests/http/test_http_proxy_tls_interception.py b/tests/http/test_http_proxy_tls_interception.py
index 1d6e7df..642f570 100644
--- a/tests/http/test_http_proxy_tls_interception.py
+++ b/tests/http/test_http_proxy_tls_interception.py
@@ -23,7 +23,7 @@ from proxy.http.handler import HttpProtocolHandler
 from proxy.http.proxy import HttpProxyPlugin
 from proxy.http.methods import httpMethods
 from proxy.common.utils import build_http_request, bytes_
-from proxy.proxy import Proxy
+from proxy.common.flag import FlagParser
 
 
 class TestHttpProxyTlsInterception(unittest.TestCase):
@@ -84,7 +84,7 @@ class TestHttpProxyTlsInterception(unittest.TestCase):
 
         self.fileno = 10
         self._addr = ('127.0.0.1', 54382)
-        self.flags = Proxy.initialize(
+        self.flags = FlagParser.initialize(
             ca_cert_file='ca-cert.pem',
             ca_key_file='ca-key.pem',
             ca_signing_key_file='ca-signing-key.pem',
diff --git a/tests/http/test_protocol_handler.py b/tests/http/test_protocol_handler.py
index bb228c0..c5af7f3 100644
--- a/tests/http/test_protocol_handler.py
+++ b/tests/http/test_protocol_handler.py
@@ -15,9 +15,9 @@ import base64
 from typing import cast
 from unittest import mock
 
-from proxy.proxy import Proxy
+from proxy.common.flag import FlagParser
 from proxy.common.version import __version__
-from proxy.common.utils import bytes_
+from proxy.common.utils import bytes_, load_plugins
 from proxy.common.constants import CRLF, PLUGIN_HTTP_PROXY, PLUGIN_PROXY_AUTH, PLUGIN_WEB_SERVER
 from proxy.core.connection import TcpClientConnection
 from proxy.http.parser import HttpParser
@@ -41,8 +41,8 @@ class TestHttpProtocolHandler(unittest.TestCase):
         self._conn = mock_fromfd.return_value
 
         self.http_server_port = 65535
-        self.flags = Proxy.initialize()
-        self.flags.plugins = Proxy.load_plugins([
+        self.flags = FlagParser.initialize()
+        self.flags.plugins = load_plugins([
             bytes_(PLUGIN_HTTP_PROXY),
             bytes_(PLUGIN_WEB_SERVER),
         ])
@@ -212,10 +212,10 @@ class TestHttpProtocolHandler(unittest.TestCase):
     ) -> None:
         self._conn = mock_fromfd.return_value
         self.mock_selector_for_client_read(mock_selector)
-        flags = Proxy.initialize(
+        flags = FlagParser.initialize(
             auth_code=base64.b64encode(b'user:pass'),
         )
-        flags.plugins = Proxy.load_plugins([
+        flags.plugins = load_plugins([
             bytes_(PLUGIN_HTTP_PROXY),
             bytes_(PLUGIN_WEB_SERVER),
             bytes_(PLUGIN_PROXY_AUTH),
@@ -250,10 +250,10 @@ class TestHttpProtocolHandler(unittest.TestCase):
         server.connect.return_value = True
         server.buffer_size.return_value = 0
 
-        flags = Proxy.initialize(
+        flags = FlagParser.initialize(
             auth_code=base64.b64encode(b'user:pass'),
         )
-        flags.plugins = Proxy.load_plugins([
+        flags.plugins = load_plugins([
             bytes_(PLUGIN_HTTP_PROXY),
             bytes_(PLUGIN_WEB_SERVER),
         ])
@@ -305,10 +305,10 @@ class TestHttpProtocolHandler(unittest.TestCase):
             mock_selector, server,
         )
 
-        flags = Proxy.initialize(
+        flags = FlagParser.initialize(
             auth_code=base64.b64encode(b'user:pass'),
         )
-        flags.plugins = Proxy.load_plugins([
+        flags.plugins = load_plugins([
             bytes_(PLUGIN_HTTP_PROXY),
             bytes_(PLUGIN_WEB_SERVER),
         ])
diff --git a/tests/http/test_web_server.py b/tests/http/test_web_server.py
index 37df71e..67eea46 100644
--- a/tests/http/test_web_server.py
+++ b/tests/http/test_web_server.py
@@ -15,11 +15,11 @@ import unittest
 import selectors
 from unittest import mock
 
-from proxy.proxy import Proxy
+from proxy.common.flag import FlagParser
 from proxy.core.connection import TcpClientConnection
 from proxy.http.handler import HttpProtocolHandler
 from proxy.http.parser import httpParserStates
-from proxy.common.utils import build_http_response, build_http_request, bytes_, text_
+from proxy.common.utils import build_http_response, build_http_request, bytes_, text_, load_plugins
 from proxy.common.constants import CRLF, PLUGIN_HTTP_PROXY, PLUGIN_PAC_FILE, PLUGIN_WEB_SERVER, PROXY_PY_DIR
 from proxy.http.server import HttpWebServerPlugin
 
@@ -33,8 +33,8 @@ class TestWebServerPlugin(unittest.TestCase):
         self._addr = ('127.0.0.1', 54382)
         self._conn = mock_fromfd.return_value
         self.mock_selector = mock_selector
-        self.flags = Proxy.initialize()
-        self.flags.plugins = Proxy.load_plugins([
+        self.flags = FlagParser.initialize()
+        self.flags.plugins = load_plugins([
             bytes_(PLUGIN_HTTP_PROXY),
             bytes_(PLUGIN_WEB_SERVER),
         ])
@@ -112,8 +112,8 @@ class TestWebServerPlugin(unittest.TestCase):
                 selectors.EVENT_READ,
             ),
         ]
-        flags = Proxy.initialize()
-        flags.plugins = Proxy.load_plugins([
+        flags = FlagParser.initialize()
+        flags.plugins = load_plugins([
             bytes_(PLUGIN_HTTP_PROXY),
             bytes_(PLUGIN_WEB_SERVER),
         ])
@@ -179,11 +179,11 @@ class TestWebServerPlugin(unittest.TestCase):
             )],
         ]
 
-        flags = Proxy.initialize(
+        flags = FlagParser.initialize(
             enable_static_server=True,
             static_server_dir=static_server_dir,
         )
-        flags.plugins = Proxy.load_plugins([
+        flags.plugins = load_plugins([
             bytes_(PLUGIN_HTTP_PROXY),
             bytes_(PLUGIN_WEB_SERVER),
         ])
@@ -246,8 +246,8 @@ class TestWebServerPlugin(unittest.TestCase):
             )],
         ]
 
-        flags = Proxy.initialize(enable_static_server=True)
-        flags.plugins = Proxy.load_plugins([
+        flags = FlagParser.initialize(enable_static_server=True)
+        flags.plugins = load_plugins([
             bytes_(PLUGIN_HTTP_PROXY),
             bytes_(PLUGIN_WEB_SERVER),
         ])
@@ -272,7 +272,7 @@ class TestWebServerPlugin(unittest.TestCase):
     def test_on_client_connection_called_on_teardown(
             self, mock_fromfd: mock.Mock,
     ) -> None:
-        flags = Proxy.initialize()
+        flags = FlagParser.initialize()
         plugin = mock.MagicMock()
         flags.plugins = {b'HttpProtocolHandlerPlugin': [plugin]}
         self._conn = mock_fromfd.return_value
@@ -289,8 +289,8 @@ class TestWebServerPlugin(unittest.TestCase):
         plugin.return_value.on_client_connection_close.assert_called()
 
     def init_and_make_pac_file_request(self, pac_file: str) -> None:
-        flags = Proxy.initialize(pac_file=pac_file)
-        flags.plugins = Proxy.load_plugins([
+        flags = FlagParser.initialize(pac_file=pac_file)
+        flags.plugins = load_plugins([
             bytes_(PLUGIN_HTTP_PROXY),
             bytes_(PLUGIN_WEB_SERVER),
             bytes_(PLUGIN_PAC_FILE),
diff --git a/tests/plugin/test_http_proxy_plugins.py b/tests/plugin/test_http_proxy_plugins.py
index 99138f1..546276d 100644
--- a/tests/plugin/test_http_proxy_plugins.py
+++ b/tests/plugin/test_http_proxy_plugins.py
@@ -17,7 +17,7 @@ from unittest import mock
 from typing import cast
 from pathlib import Path
 
-from proxy.proxy import Proxy
+from proxy.common.flag import FlagParser
 from proxy.core.connection import TcpClientConnection
 from proxy.http.handler import HttpProtocolHandler
 from proxy.http.proxy import HttpProxyPlugin
@@ -44,7 +44,7 @@ class TestHttpProxyPluginExamples(unittest.TestCase):
         adblock_json_path = Path(
             __file__,
         ).parent.parent.parent / "proxy" / "plugin" / "adblock.json"
-        self.flags = Proxy.initialize(
+        self.flags = FlagParser.initialize(
             input_args=[
                 "--filtered-url-regex-config",
                 str(adblock_json_path),
diff --git a/tests/plugin/test_http_proxy_plugins_with_tls_interception.py b/tests/plugin/test_http_proxy_plugins_with_tls_interception.py
index 48d7d1c..39351fd 100644
--- a/tests/plugin/test_http_proxy_plugins_with_tls_interception.py
+++ b/tests/plugin/test_http_proxy_plugins_with_tls_interception.py
@@ -16,9 +16,8 @@ import ssl
 from unittest import mock
 from typing import Any, cast
 
-from proxy.proxy import Proxy
-from proxy.common.utils import bytes_
-from proxy.common.utils import build_http_request, build_http_response
+from proxy.common.flag import FlagParser
+from proxy.common.utils import bytes_, build_http_request, build_http_response
 from proxy.core.connection import TcpClientConnection, TcpServerConnection
 from proxy.http.codes import httpStatusCodes
 from proxy.http.methods import httpMethods
@@ -64,7 +63,7 @@ class TestHttpProxyPluginExamplesWithTlsInterception(unittest.TestCase):
 
         self.fileno = 10
         self._addr = ('127.0.0.1', 54382)
-        self.flags = Proxy.initialize(
+        self.flags = FlagParser.initialize(
             ca_cert_file='ca-cert.pem',
             ca_key_file='ca-key.pem',
             ca_signing_key_file='ca-signing-key.pem',
diff --git a/tests/test_main.py b/tests/test_main.py
index 8f4ffba..92b3234 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -14,9 +14,10 @@ import os
 
 from unittest import mock
 
-from proxy.proxy import main, Proxy, entry_point
+from proxy.proxy import main, entry_point
 from proxy.common.utils import bytes_
 from proxy.http.handler import HttpProtocolHandler
+from proxy.common.flag import FlagParser
 
 from proxy.common.constants import DEFAULT_ENABLE_DASHBOARD, DEFAULT_LOG_LEVEL, DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT
 from proxy.common.constants import DEFAULT_TIMEOUT, DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HTTP_PROXY
@@ -68,7 +69,7 @@ class TestMain(unittest.TestCase):
         mock_args.enable_dashboard = DEFAULT_ENABLE_DASHBOARD
 
     @mock.patch('time.sleep')
-    @mock.patch('proxy.proxy.Proxy.initialize')
+    @mock.patch('proxy.proxy.FlagParser.initialize')
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
     def test_entry_point(
@@ -92,7 +93,7 @@ class TestMain(unittest.TestCase):
         mock_sleep.assert_called()
 
     @mock.patch('time.sleep')
-    @mock.patch('proxy.proxy.Proxy.initialize')
+    @mock.patch('proxy.proxy.FlagParser.initialize')
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
     def test_main_with_no_arguments(
@@ -116,7 +117,7 @@ class TestMain(unittest.TestCase):
         mock_sleep.assert_called()
 
     @mock.patch('time.sleep')
-    @mock.patch('proxy.proxy.Proxy.initialize')
+    @mock.patch('proxy.proxy.FlagParser.initialize')
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
     def test_enable_events(
@@ -142,7 +143,7 @@ class TestMain(unittest.TestCase):
         mock_sleep.assert_called()
 
     @mock.patch('time.sleep')
-    @mock.patch('proxy.proxy.Proxy.load_plugins')
+    @mock.patch('proxy.common.flag.load_plugins')
     @mock.patch('proxy.common.flag.FlagParser.parse_args')
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
@@ -178,7 +179,7 @@ class TestMain(unittest.TestCase):
         mock_event_manager.return_value.stop_event_dispatcher.assert_called_once()
 
     @mock.patch('time.sleep')
-    @mock.patch('proxy.proxy.Proxy.load_plugins')
+    @mock.patch('proxy.common.flag.load_plugins')
     @mock.patch('proxy.common.flag.FlagParser.parse_args')
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
@@ -256,7 +257,7 @@ class TestMain(unittest.TestCase):
         mock_sleep.side_effect = KeyboardInterrupt()
 
         input_args = ['--basic-auth', 'user:pass']
-        flgs = Proxy.initialize(input_args)
+        flgs = FlagParser.initialize(input_args)
 
         main(input_args=input_args)
         mock_event_manager.assert_not_called()
@@ -270,7 +271,7 @@ class TestMain(unittest.TestCase):
     @mock.patch('builtins.print')
     @mock.patch('proxy.proxy.EventManager')
     @mock.patch('proxy.proxy.AcceptorPool')
-    @mock.patch('proxy.proxy.Proxy.is_py2')
+    @mock.patch('proxy.common.flag.is_py2')
     def test_main_py3_runs(
             self,
             mock_is_py2: mock.Mock,
@@ -294,7 +295,7 @@ class TestMain(unittest.TestCase):
         mock_acceptor_pool.return_value.setup.assert_called()
 
     @mock.patch('builtins.print')
-    @mock.patch('proxy.proxy.Proxy.is_py2')
+    @mock.patch('proxy.common.flag.is_py2')
     def test_main_py2_exit(
             self,
             mock_is_py2: mock.Mock,
diff --git a/tests/test_set_open_file_limit.py b/tests/test_set_open_file_limit.py
index 1163564..35fcc30 100644
--- a/tests/test_set_open_file_limit.py
+++ b/tests/test_set_open_file_limit.py
@@ -12,7 +12,7 @@ import os
 import unittest
 from unittest import mock
 
-from proxy.proxy import Proxy
+from proxy.common.utils import set_open_file_limit
 
 if os.name != 'nt':
     import resource
@@ -31,7 +31,7 @@ class TestSetOpenFileLimit(unittest.TestCase):
             mock_set_rlimit: mock.Mock,
             mock_get_rlimit: mock.Mock,
     ) -> None:
-        Proxy.set_open_file_limit(256)
+        set_open_file_limit(256)
         mock_get_rlimit.assert_called_with(resource.RLIMIT_NOFILE)
         mock_set_rlimit.assert_called_with(resource.RLIMIT_NOFILE, (256, 1024))
 
@@ -42,7 +42,7 @@ class TestSetOpenFileLimit(unittest.TestCase):
             mock_set_rlimit: mock.Mock,
             mock_get_rlimit: mock.Mock,
     ) -> None:
-        Proxy.set_open_file_limit(256)
+        set_open_file_limit(256)
         mock_get_rlimit.assert_called_with(resource.RLIMIT_NOFILE)
         mock_set_rlimit.assert_not_called()
 
@@ -53,6 +53,6 @@ class TestSetOpenFileLimit(unittest.TestCase):
             mock_set_rlimit: mock.Mock,
             mock_get_rlimit: mock.Mock,
     ) -> None:
-        Proxy.set_open_file_limit(1024)
+        set_open_file_limit(1024)
         mock_get_rlimit.assert_called_with(resource.RLIMIT_NOFILE)
         mock_set_rlimit.assert_not_called()