Repository: abhinavsingh/proxy.py
Pull request #172 (2019-11-13 19:49:51)

Allow to pass flags as kwargs too in embed mode
+ Allow to pass flags as kwargs too in embed mode
+ load dev tools in an iframe to avoid polluting parent window namespace.

codecov[bot]: # [Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/172?src=pr&el=h1) Report
> Merging [#172](https://codecov.io/gh/abhinavsingh/proxy.py/pull/172?src=pr&el=desc) into [develop](https://codecov.io/gh/abhinavsingh/proxy.py/commit/1b0c2f8c5eee11f3048018b407ec7f553afbd891?src=pr&el=desc) will **increase** coverage by `0.05%`.
> The diff coverage is `93.75%`.

[![Impacted file tree graph](https://codecov.io/gh/abhinavsingh/proxy.py/pull/172/graphs/tree.svg?width=650&token=Zh9J7b4la2&height=150&src=pr)](https://codecov.io/gh/abhinavsingh/proxy.py/pull/172?src=pr&el=tree)

```diff
@@             Coverage Diff             @@
##           develop     #172      +/-   ##
===========================================
+ Coverage    76.38%   76.44%   +0.05%     
===========================================
  Files           26       25       -1     
  Lines         2024     2029       +5     
===========================================
+ Hits          1546     1551       +5     
  Misses         478      478
```


| [Impacted Files](https://codecov.io/gh/abhinavsingh/proxy.py/pull/172?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [proxy/\_\_main\_\_.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/172/diff?src=pr&el=tree#diff-cHJveHkvX19tYWluX18ucHk=) | `0% <0%> (ø)` | :arrow_up: |
| [proxy/common/flags.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/172/diff?src=pr&el=tree#diff-cHJveHkvY29tbW9uL2ZsYWdzLnB5) | `95.32% <92.79%> (-4.68%)` | :arrow_down: |
| [proxy/\_\_init\_\_.py](https://codecov.io/gh/abhinavsingh/proxy.py/pull/172/diff?src=pr&el=tree#diff-cHJveHkvX19pbml0X18ucHk=) | `97.95% <97.91%> (-2.05%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/172?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/abhinavsingh/proxy.py/pull/172?src=pr&el=footer). Last update [1b0c2f8...52474c9](https://codecov.io/gh/abhinavsingh/proxy.py/pull/172?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/README.md b/README.md
index 7db4870..5539959 100644
--- a/README.md
+++ b/README.md
@@ -58,7 +58,7 @@ Table of Contents
     * [Blocking Mode](#blocking-mode)
     * [Non-blocking Mode](#non-blocking-mode)
 * [Unit testing with proxy.py](#unit-testing-with-proxypy)
-    * [proxy.main.TestCase](#proxymaintestcase)
+    * [proxy.TestCase](#proxytestcase)
     * [Override Startup Flags](#override-startup-flags)
     * [With unittest.TestCase](#with-unittesttestcase)
 * [Plugin Developer and Contributor Guide](#plugin-developer-and-contributor-guide)
@@ -723,27 +723,41 @@ Embed proxy.py
 
 ## Blocking Mode
 
-Start `proxy.py` in embedded mode by using `main` method:
+Start `proxy.py` in embedded mode with default configuration
+by using the `main` method. Example:
 
 ```
-from proxy.main import main
+import proxy
 
 if __name__ == '__main__':
-  main([])
+  proxy.main()
 ```
 
-or, to start with arguments:
+Customize startup flags by passing list of input arguments:
 
 ```
-from proxy.main import main
+import proxy
 
 if __name__ == '__main__':
-  main([
+  proxy.main([
     '--hostname', '::1',
     '--port', '8899'
   ])
 ```
 
+or, customize startup flags by passing them as kwargs:
+
+```
+import ipaddress
+import proxy
+
+if __name__ == '__main__':
+  proxy.main(
+    hostname=ipaddress.IPv6Address('::1'),
+    port=8899
+  )
+```
+
 Note that:
 
 1. Calling `main` is simply equivalent to starting `proxy.py` from command line.
@@ -751,36 +765,42 @@ Note that:
 
 ## Non-blocking Mode
 
-Start `proxy.py` in embedded mode by using `start` method:
+Start `proxy.py` in non-blocking embedded mode with default configuration
+by using `start` method:  Example:
 
 ```
-from proxy.main import start
+import proxy
 
 if __name__ == '__main__':
-  with start([]):
+  with proxy.start([]):
     # ... your logic here ...
 ```
 
 Note that:
 
-1. `start` is simply a context manager.
-2. Is similar to calling `main` except `start` won't block.
-3. It automatically shut down `proxy.py`.
+1. `start` is similar to `main`, except `start` won't block.
+1. `start` is a context manager.
+   It will start `proxy.py` when called and will shut it down
+   once scope ends.
+3. Just like `main`, startup flags with `start` method too
+   can be customized by either passing flags as list of
+   input arguments `start(['--port', '8899'])` or by using passing
+   flags as kwargs `start(port=8899)`.
 
 Unit testing with proxy.py
 ==========================
 
-## proxy.main.TestCase
+## proxy.TestCase
 
 To setup and teardown `proxy.py` for your Python unittest classes,
-simply use `proxy.main.TestCase` instead of `unittest.TestCase`.
+simply use `proxy.TestCase` instead of `unittest.TestCase`.
 Example:
 
 ```
-from proxy.main import TestCase
+import proxy
 
 
-class TestProxyPyEmbedded(TestCase):
+class TestProxyPyEmbedded(proxy.TestCase):
 
     def test_my_application_with_proxy(self) -> None:
         self.assertTrue(True)
@@ -788,10 +808,12 @@ class TestProxyPyEmbedded(TestCase):
 
 Note that:
 
-1. `proxy.main.TestCase` overrides `unittest.TestCase.run()` method to setup and teardown `proxy.py`.
+1. `proxy.TestCase` overrides `unittest.TestCase.run()` method to setup and teardown `proxy.py`.
 2. `proxy.py` server will listen on a random available port on the system.
    This random port is available as `self.proxy_port` within your test cases.
 3. Only a single worker is started by default (`--num-workers 1`) for faster setup and teardown.
+4. Most importantly, `proxy.TestCase` also ensures `proxy.py` server
+   is up and running before proceeding with execution of tests.
 
 ## Override startup flags
 
@@ -815,14 +837,13 @@ for full working example.
 
 ## With unittest.TestCase
 
-If for some reasons you are unable to directly use `proxy.main.TestCase`,
+If for some reasons you are unable to directly use `proxy.TestCase`,
 then simply override `unittest.TestCase.run` yourself to setup and teardown `proxy.py`.
 Example:
 
 ```
 import unittest
-
-from proxy.main import start
+import proxy
 
 
 class TestProxyPyEmbedded(unittest.TestCase):
@@ -831,7 +852,7 @@ class TestProxyPyEmbedded(unittest.TestCase):
         self.assertTrue(True)
 
     def run(self, result: Optional[unittest.TestResult] = None) -> Any:
-        with start([
+        with proxy.start([
                 '--num-workers', '1',
                 '--port', '... random port ...']):
             super().run(result)
diff --git a/benchmark/benchmark.py b/benchmark/benchmark.py
index 1a75fd8..4ee78cf 100755
--- a/benchmark/benchmark.py
+++ b/benchmark/benchmark.py
@@ -15,11 +15,13 @@ import sys
 import time
 from typing import List, Tuple
 
-from proxy.common.constants import __homepage__, DEFAULT_BUFFER_SIZE
+from proxy.common.constants import DEFAULT_BUFFER_SIZE
 from proxy.common.utils import build_http_request
 from proxy.http.methods import httpMethods
 from proxy.http.parser import httpParserStates, httpParserTypes, HttpParser
 
+__homepage__ = 'https://github.com/abhinavsingh/proxy.py'
+
 DEFAULT_N = 1
 
 
diff --git a/dashboard/__init__.py b/dashboard/__init__.py
index f70d512..87af918 100644
--- a/dashboard/__init__.py
+++ b/dashboard/__init__.py
@@ -8,4 +8,3 @@
     :copyright: (c) 2013-present by Abhinav Singh and contributors.
     :license: BSD, see LICENSE for more details.
 """
-from .dashboard import __version__
diff --git a/dashboard/dashboard.py b/dashboard/dashboard.py
index b9e0fd7..378c88a 100644
--- a/dashboard/dashboard.py
+++ b/dashboard/dashboard.py
@@ -24,9 +24,6 @@ from proxy.common.utils import build_http_response, bytes_
 from proxy.common.types import DictQueueType
 from proxy.core.connection import TcpClientConnection
 
-VERSION = (0, 1, 0)
-__version__ = '.'.join(map(str, VERSION[0:3]))
-
 logger = logging.getLogger(__name__)
 
 
diff --git a/dashboard/setup.py b/dashboard/setup.py
index f0413a6..f88e0f6 100644
--- a/dashboard/setup.py
+++ b/dashboard/setup.py
@@ -10,10 +10,14 @@
 """
 from setuptools import setup, find_packages
 
-from proxy.common.constants import __author__, __author_email__
-from proxy.common.constants import __homepage__, __description__, __download_url__, __license__
-
-from dashboard import __version__
+VERSION = (0, 1, 0)
+__version__ = '.'.join(map(str, VERSION[0:3]))
+__description__ = '⚡⚡⚡ Fast, Lightweight, Programmable Proxy Server in a single Python file.'
+__author__ = 'Abhinav Singh'
+__author_email__ = 'mailsforabhinav@gmail.com'
+__homepage__ = 'https://github.com/abhinavsingh/proxy.py'
+__download_url__ = '%s/archive/master.zip' % __homepage__
+__license__ = 'BSD'
 
 setup(
     name='proxy.py-dashboard',
diff --git a/dashboard/src/core/plugins/inspect_traffic.ts b/dashboard/src/core/plugins/inspect_traffic.ts
index e7174df..6f25d9e 100644
--- a/dashboard/src/core/plugins/inspect_traffic.ts
+++ b/dashboard/src/core/plugins/inspect_traffic.ts
@@ -29,6 +29,7 @@ export class InspectTrafficPlugin extends DashboardPlugin {
 
   public activated (): void {
     this.websocketApi.enableInspection(this.handleEvents.bind(this))
+    this.ensureIFrame()
   }
 
   public deactivated (): void {
@@ -39,10 +40,27 @@ export class InspectTrafficPlugin extends DashboardPlugin {
     console.log(message)
   }
 
+  private ensureIFrame (): void {
+    if ($('#' + this.getDevtoolsIFrameID()).length === 0) {
+      $('#' + this.name)
+        .children('.app-body')
+        .append(
+          this.getDevtoolsIFrame()
+        )
+    }
+  }
+
+  private getDevtoolsIFrameID (): string {
+    return this.name.concat('_inspector')
+  }
+
   private getDevtoolsIFrame (): JQuery<HTMLElement> {
     return $('<iframe></iframe>')
+      .attr('id', this.getDevtoolsIFrameID())
       .attr('height', '80%')
       .attr('width', '100%')
+      .attr('padding', '0')
+      .attr('margin', '0')
       .attr('frameBorder', '0')
       .attr('scrolling', 'no')
       .attr('src', 'devtools/inspect_traffic.html')
diff --git a/plugin_examples/setup.py b/plugin_examples/setup.py
index 18899f2..5e23a56 100644
--- a/plugin_examples/setup.py
+++ b/plugin_examples/setup.py
@@ -10,11 +10,14 @@
 """
 from setuptools import setup, find_packages
 
-from proxy.common.constants import __author__, __author_email__
-from proxy.common.constants import __homepage__, __description__, __download_url__, __license__
-
 VERSION = (0, 1, 0)
 __version__ = '.'.join(map(str, VERSION[0:3]))
+__description__ = '⚡⚡⚡ Fast, Lightweight, Programmable Proxy Server in a single Python file.'
+__author__ = 'Abhinav Singh'
+__author_email__ = 'mailsforabhinav@gmail.com'
+__homepage__ = 'https://github.com/abhinavsingh/proxy.py'
+__download_url__ = '%s/archive/master.zip' % __homepage__
+__license__ = 'BSD'
 
 setup(
     name='proxy.py-plugins',
diff --git a/proxy/__init__.py b/proxy/__init__.py
old mode 100644
new mode 100755
index ba03413..e496b84
--- a/proxy/__init__.py
+++ b/proxy/__init__.py
@@ -7,3 +7,17 @@
     :copyright: (c) 2013-present by Abhinav Singh and contributors.
     :license: BSD, see LICENSE for more details.
 """
+from .proxy import entry_point
+from .proxy import main, start
+from .proxy import TestCase
+
+__all__ = [
+    # PyPi package entry_point.  See
+    # https://github.com/abhinavsingh/proxy.py#from-command-line-when-installed-using-pip
+    'entry_point',
+    # Embed mode.  See https://github.com/abhinavsingh/proxy.py#embed-proxypy
+    'main', 'start',
+    # Unit testing with proxy.py.  See
+    # https://github.com/abhinavsingh/proxy.py#unit-testing-with-proxypy
+    'TestCase'
+]
diff --git a/proxy/__main__.py b/proxy/__main__.py
index 50e913d..1970354 100644
--- a/proxy/__main__.py
+++ b/proxy/__main__.py
@@ -7,7 +7,7 @@
     :copyright: (c) 2013-present by Abhinav Singh and contributors.
     :license: BSD, see LICENSE for more details.
 """
-from .main import entry_point
+from .proxy import entry_point
 
 if __name__ == '__main__':
     entry_point()
diff --git a/proxy/common/constants.py b/proxy/common/constants.py
index fad7b97..468b119 100644
--- a/proxy/common/constants.py
+++ b/proxy/common/constants.py
@@ -15,13 +15,6 @@ from typing import List
 
 from .version import __version__
 
-__description__ = '⚡⚡⚡ Fast, Lightweight, Programmable Proxy Server in a single Python file.'
-__author__ = 'Abhinav Singh'
-__author_email__ = 'mailsforabhinav@gmail.com'
-__homepage__ = 'https://github.com/abhinavsingh/proxy.py'
-__download_url__ = '%s/archive/master.zip' % __homepage__
-__license__ = 'BSD'
-
 PROXY_PY_DIR = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
 PROXY_PY_START_TIME = time.time()
 
diff --git a/proxy/common/flags.py b/proxy/common/flags.py
index a49ba39..efd5c58 100644
--- a/proxy/common/flags.py
+++ b/proxy/common/flags.py
@@ -7,16 +7,21 @@
     :copyright: (c) 2013-present by Abhinav Singh and contributors.
     :license: BSD, see LICENSE for more details.
 """
+import logging
+import importlib
 import argparse
+import base64
 import ipaddress
 import os
 import socket
 import multiprocessing
+import sys
+import inspect
 import pathlib
 
-from typing import Optional, Union, Dict, List
+from typing import Optional, Union, Dict, List, TypeVar, Type, cast, Any
 
-from .utils import text_
+from .utils import text_, bytes_
 from .types import DictQueueType
 from .constants import DEFAULT_LOG_LEVEL, DEFAULT_LOG_FILE, DEFAULT_LOG_FORMAT, DEFAULT_BACKLOG, DEFAULT_BASIC_AUTH
 from .constants import DEFAULT_TIMEOUT, DEFAULT_DEVTOOLS_WS_PATH, DEFAULT_DISABLE_HTTP_PROXY, DEFAULT_DISABLE_HEADERS
@@ -26,212 +31,17 @@ from .constants import DEFAULT_CA_CERT_DIR, DEFAULT_CA_CERT_FILE, DEFAULT_CA_KEY
 from .constants import DEFAULT_PAC_FILE_URL_PATH, DEFAULT_PAC_FILE, DEFAULT_PLUGINS, DEFAULT_PID_FILE, DEFAULT_PORT
 from .constants import DEFAULT_NUM_WORKERS, DEFAULT_VERSION, DEFAULT_OPEN_FILE_LIMIT, DEFAULT_IPV6_HOSTNAME
 from .constants import DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_CLIENT_RECVBUF_SIZE, DEFAULT_STATIC_SERVER_DIR
-from .constants import COMMA
-from .constants import __homepage__
+from .constants import COMMA, DOT
 from .version import __version__
 
+__homepage__ = 'https://github.com/abhinavsingh/proxy.py'
 
-def init_parser() -> argparse.ArgumentParser:
-    """Initializes and returns argument parser."""
-    parser = argparse.ArgumentParser(
-        description='proxy.py v%s' % __version__,
-        epilog='Proxy.py not working? Report at: %s/issues/new' % __homepage__
-    )
-    # Argument names are ordered alphabetically.
-    parser.add_argument(
-        '--backlog',
-        type=int,
-        default=DEFAULT_BACKLOG,
-        help='Default: 100. Maximum number of pending connections to proxy server')
-    parser.add_argument(
-        '--basic-auth',
-        type=str,
-        default=DEFAULT_BASIC_AUTH,
-        help='Default: No authentication. Specify colon separated user:password '
-             'to enable basic authentication.')
-    parser.add_argument(
-        '--ca-key-file',
-        type=str,
-        default=DEFAULT_CA_KEY_FILE,
-        help='Default: None. CA key to use for signing dynamically generated '
-             'HTTPS certificates.  If used, must also pass --ca-cert-file and --ca-signing-key-file'
-    )
-    parser.add_argument(
-        '--ca-cert-dir',
-        type=str,
-        default=DEFAULT_CA_CERT_DIR,
-        help='Default: ~/.proxy.py. Directory to store dynamically generated certificates. '
-             'Also see --ca-key-file, --ca-cert-file and --ca-signing-key-file'
-    )
-    parser.add_argument(
-        '--ca-cert-file',
-        type=str,
-        default=DEFAULT_CA_CERT_FILE,
-        help='Default: None. Signing certificate to use for signing dynamically generated '
-             'HTTPS certificates.  If used, must also pass --ca-key-file and --ca-signing-key-file'
-    )
-    parser.add_argument(
-        '--ca-signing-key-file',
-        type=str,
-        default=DEFAULT_CA_SIGNING_KEY_FILE,
-        help='Default: None. CA signing key to use for dynamic generation of '
-             'HTTPS certificates.  If used, must also pass --ca-key-file and --ca-cert-file'
-    )
-    parser.add_argument(
-        '--cert-file',
-        type=str,
-        default=DEFAULT_CERT_FILE,
-        help='Default: None. Server certificate to enable end-to-end TLS encryption with clients. '
-             'If used, must also pass --key-file.'
-    )
-    parser.add_argument(
-        '--client-recvbuf-size',
-        type=int,
-        default=DEFAULT_CLIENT_RECVBUF_SIZE,
-        help='Default: 1 MB. Maximum amount of data received from the '
-             'client in a single recv() operation. Bump this '
-             'value for faster uploads at the expense of '
-             'increased RAM.')
-    parser.add_argument(
-        '--devtools-ws-path',
-        type=str,
-        default=DEFAULT_DEVTOOLS_WS_PATH,
-        help='Default: /devtools.  Only applicable '
-             'if --enable-devtools is used.'
-    )
-    parser.add_argument(
-        '--disable-headers',
-        type=str,
-        default=COMMA.join(DEFAULT_DISABLE_HEADERS),
-        help='Default: None.  Comma separated list of headers to remove before '
-             'dispatching client request to upstream server.')
-    parser.add_argument(
-        '--disable-http-proxy',
-        action='store_true',
-        default=DEFAULT_DISABLE_HTTP_PROXY,
-        help='Default: False.  Whether to disable proxy.HttpProxyPlugin.')
-    parser.add_argument(
-        '--enable-devtools',
-        action='store_true',
-        default=DEFAULT_ENABLE_DEVTOOLS,
-        help='Default: False.  Enables integration with Chrome Devtool Frontend.'
-    )
-    parser.add_argument(
-        '--enable-events',
-        action='store_true',
-        default=DEFAULT_ENABLE_EVENTS,
-        help='Default: False.  Enables core to dispatch lifecycle events. '
-             'Plugins can be used to subscribe for core events.'
-    )
-    parser.add_argument(
-        '--enable-static-server',
-        action='store_true',
-        default=DEFAULT_ENABLE_STATIC_SERVER,
-        help='Default: False.  Enable inbuilt static file server. '
-             'Optionally, also use --static-server-dir to serve static content '
-             'from custom directory.  By default, static file server serves '
-             'from public folder.'
-    )
-    parser.add_argument(
-        '--enable-web-server',
-        action='store_true',
-        default=DEFAULT_ENABLE_WEB_SERVER,
-        help='Default: False.  Whether to enable proxy.HttpWebServerPlugin.')
-    parser.add_argument(
-        '--hostname',
-        type=str,
-        default=str(DEFAULT_IPV6_HOSTNAME),
-        help='Default: ::1. Server IP address.')
-    parser.add_argument(
-        '--key-file',
-        type=str,
-        default=DEFAULT_KEY_FILE,
-        help='Default: None. Server key file to enable end-to-end TLS encryption with clients. '
-             'If used, must also pass --cert-file.'
-    )
-    parser.add_argument(
-        '--log-level',
-        type=str,
-        default=DEFAULT_LOG_LEVEL,
-        help='Valid options: DEBUG, INFO (default), WARNING, ERROR, CRITICAL. '
-             'Both upper and lowercase values are allowed. '
-             'You may also simply use the leading character e.g. --log-level d')
-    parser.add_argument('--log-file', type=str, default=DEFAULT_LOG_FILE,
-                        help='Default: sys.stdout. Log file destination.')
-    parser.add_argument('--log-format', type=str, default=DEFAULT_LOG_FORMAT,
-                        help='Log format for Python logger.')
-    parser.add_argument('--num-workers', type=int, default=DEFAULT_NUM_WORKERS,
-                        help='Defaults to number of CPU cores.')
-    parser.add_argument(
-        '--open-file-limit',
-        type=int,
-        default=DEFAULT_OPEN_FILE_LIMIT,
-        help='Default: 1024. Maximum number of files (TCP connections) '
-             'that proxy.py can open concurrently.')
-    parser.add_argument(
-        '--pac-file',
-        type=str,
-        default=DEFAULT_PAC_FILE,
-        help='A file (Proxy Auto Configuration) or string to serve when '
-             'the server receives a direct file request. '
-             'Using this option enables proxy.HttpWebServerPlugin.')
-    parser.add_argument(
-        '--pac-file-url-path',
-        type=str,
-        default=text_(DEFAULT_PAC_FILE_URL_PATH),
-        help='Default: %s. Web server path to serve the PAC file.' %
-             text_(DEFAULT_PAC_FILE_URL_PATH))
-    parser.add_argument(
-        '--pid-file',
-        type=str,
-        default=DEFAULT_PID_FILE,
-        help='Default: None. Save parent process ID to a file.')
-    parser.add_argument(
-        '--plugins',
-        type=str,
-        default=DEFAULT_PLUGINS,
-        help='Comma separated plugins')
-    parser.add_argument('--port', type=int, default=DEFAULT_PORT,
-                        help='Default: 8899. Server port.')
-    parser.add_argument(
-        '--server-recvbuf-size',
-        type=int,
-        default=DEFAULT_SERVER_RECVBUF_SIZE,
-        help='Default: 1 MB. Maximum amount of data received from the '
-             'server in a single recv() operation. Bump this '
-             'value for faster downloads at the expense of '
-             'increased RAM.')
-    parser.add_argument(
-        '--static-server-dir',
-        type=str,
-        default=DEFAULT_STATIC_SERVER_DIR,
-        help='Default: "public" folder in directory where proxy.py is placed. '
-             'This option is only applicable when static server is also enabled. '
-             'See --enable-static-server.'
-    )
-    parser.add_argument(
-        '--threadless',
-        action='store_true',
-        default=DEFAULT_THREADLESS,
-        help='Default: False.  When disabled a new thread is spawned '
-             'to handle each client connection.'
-    )
-    parser.add_argument(
-        '--timeout',
-        type=int,
-        default=DEFAULT_TIMEOUT,
-        help='Default: ' + str(DEFAULT_TIMEOUT) +
-        '.  Number of seconds after which '
-             'an inactive connection must be dropped.  Inactivity is defined by no '
-             'data sent or received by the client.'
-    )
-    parser.add_argument(
-        '--version',
-        '-v',
-        action='store_true',
-        default=DEFAULT_VERSION,
-        help='Prints proxy.py version.')
-    return parser
+if os.name != 'nt':
+    import resource
+
+logger = logging.getLogger(__name__)
+
+T = TypeVar('T', bound='Flags')
 
 
 class Flags:
@@ -266,7 +76,9 @@ class Flags:
             devtools_ws_path: bytes = DEFAULT_DEVTOOLS_WS_PATH,
             timeout: int = DEFAULT_TIMEOUT,
             threadless: bool = DEFAULT_THREADLESS,
-            enable_events: bool = DEFAULT_ENABLE_EVENTS) -> None:
+            enable_events: bool = DEFAULT_ENABLE_EVENTS,
+            pid_file: Optional[str] = DEFAULT_PID_FILE) -> None:
+        self.pid_file = pid_file
         self.threadless = threadless
         self.timeout = timeout
         self.auth_code = auth_code
@@ -310,6 +122,137 @@ class Flags:
                 self.proxy_py_data_dir, self.GENERATED_CERTS_DIR_NAME)
             os.makedirs(self.ca_cert_dir, exist_ok=True)
 
+    @classmethod
+    def initialize(
+            cls: Type[T],
+            input_args: Optional[List[str]],
+            **opts: Any) -> T:
+        if not Flags.is_py3():
+            print(
+                'DEPRECATION: "develop" branch no longer supports Python 2.7.  Kindly upgrade to Python 3+. '
+                'If for some reasons you cannot upgrade, consider using "master" branch or simply '
+                '"pip install proxy.py==0.3".'
+                '\n\n'
+                'DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. '
+                'Please upgrade your Python as Python 2.7 won\'t be maintained after that date. '
+                'A future version of pip will drop support for Python 2.7.')
+            sys.exit(1)
+
+        args = Flags.init_parser().parse_args(input_args)
+
+        if args.version:
+            print(__version__)
+            sys.exit(0)
+
+        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)
+
+        auth_code = None
+        if args.basic_auth:
+            auth_code = b'Basic %s' % base64.b64encode(bytes_(args.basic_auth))
+
+        Flags.setup_logger(args.log_file, args.log_level, args.log_format)
+        Flags.set_open_file_limit(args.open_file_limit)
+
+        default_plugins = ''
+        devtools_event_queue: Optional[DictQueueType] = None
+        if args.enable_devtools:
+            default_plugins += 'proxy.http.devtools.DevtoolsProtocolPlugin,'
+            default_plugins += 'proxy.http.server.HttpWebServerPlugin,'
+        if not args.disable_http_proxy:
+            default_plugins += 'proxy.http.proxy.HttpProxyPlugin,'
+        if args.enable_web_server or \
+                args.pac_file is not None or \
+                args.enable_static_server:
+            if 'proxy.http.server.HttpWebServerPlugin' not in default_plugins:
+                default_plugins += 'proxy.http.server.HttpWebServerPlugin,'
+        if args.enable_devtools:
+            default_plugins += 'proxy.http.devtools.DevtoolsWebsocketPlugin,'
+            devtools_event_queue = multiprocessing.Manager().Queue()
+        if args.pac_file is not None:
+            default_plugins += 'proxy.http.server.HttpWebServerPacFilePlugin,'
+
+        return cls(
+            auth_code=cast(Optional[bytes], opts.get('auth_code', auth_code)),
+            server_recvbuf_size=cast(
+                int,
+                opts.get(
+                    'server_recvbuf_size',
+                    args.server_recvbuf_size)),
+            client_recvbuf_size=cast(
+                int,
+                opts.get(
+                    'client_recvbuf_size',
+                    args.client_recvbuf_size)),
+            pac_file=cast(
+                Optional[str], opts.get(
+                    'pac_file', bytes_(
+                        args.pac_file))),
+            pac_file_url_path=cast(
+                Optional[bytes], opts.get(
+                    'pac_file_url_path', bytes_(
+                        args.pac_file_url_path))),
+            disable_headers=cast(Optional[List[bytes]], opts.get('disable_headers', [
+                header.lower() for header in bytes_(
+                    args.disable_headers).split(COMMA) if header.strip() != b''])),
+            certfile=cast(
+                Optional[str], opts.get(
+                    'cert_file', args.cert_file)),
+            keyfile=cast(Optional[str], opts.get('key_file', args.key_file)),
+            ca_cert_dir=cast(
+                Optional[str], opts.get(
+                    'ca_cert_dir', args.ca_cert_dir)),
+            ca_key_file=cast(
+                Optional[str], opts.get(
+                    'ca_key_file', args.ca_key_file)),
+            ca_cert_file=cast(
+                Optional[str], opts.get(
+                    'ca_cert_file', args.ca_cert_file)),
+            ca_signing_key_file=cast(
+                Optional[str],
+                opts.get(
+                    'ca_signing_key_file',
+                    args.ca_signing_key_file)),
+            hostname=cast(Union[ipaddress.IPv4Address,
+                                ipaddress.IPv6Address],
+                          opts.get('hostname', ipaddress.ip_address(args.hostname))),
+            port=cast(int, opts.get('port', args.port)),
+            backlog=cast(int, opts.get('backlog', args.backlog)),
+            num_workers=cast(int, opts.get('num_workers', args.num_workers)),
+            static_server_dir=cast(
+                str,
+                opts.get(
+                    'static_server_dir',
+                    args.static_server_dir)),
+            enable_static_server=cast(
+                bool,
+                opts.get(
+                    'enable_static_server',
+                    args.enable_static_server)),
+            devtools_ws_path=cast(
+                bytes,
+                opts.get(
+                    'devtools_ws_path',
+                    args.devtools_ws_path)),
+            timeout=cast(int, opts.get('timeout', args.timeout)),
+            threadless=cast(bool, opts.get('threadless', args.threadless)),
+            enable_events=cast(
+                bool,
+                opts.get(
+                    'enable_events',
+                    args.enable_events)),
+            devtools_event_queue=cast(
+                Optional[DictQueueType], opts.get(
+                    'devtools_event_queue', devtools_event_queue)),
+            plugins=Flags.load_plugins(
+                bytes_(
+                    '%s%s' %
+                    (default_plugins, opts.get('plugins', args.plugins)))),
+            pid_file=cast(Optional[str], opts.get('pid_file', args.pid_file)))
+
     def tls_interception_enabled(self) -> bool:
         return self.ca_key_file is not None and \
             self.ca_cert_dir is not None and \
@@ -319,3 +262,274 @@ class Flags:
     def encryption_enabled(self) -> bool:
         return self.keyfile is not None and \
             self.certfile is not None
+
+    @staticmethod
+    def init_parser() -> argparse.ArgumentParser:
+        """Initializes and returns argument parser."""
+        parser = argparse.ArgumentParser(
+            description='proxy.py v%s' % __version__,
+            epilog='Proxy.py not working? Report at: %s/issues/new' % __homepage__
+        )
+        # Argument names are ordered alphabetically.
+        parser.add_argument(
+            '--backlog',
+            type=int,
+            default=DEFAULT_BACKLOG,
+            help='Default: 100. Maximum number of pending connections to proxy server')
+        parser.add_argument(
+            '--basic-auth',
+            type=str,
+            default=DEFAULT_BASIC_AUTH,
+            help='Default: No authentication. Specify colon separated user:password '
+                 'to enable basic authentication.')
+        parser.add_argument(
+            '--ca-key-file',
+            type=str,
+            default=DEFAULT_CA_KEY_FILE,
+            help='Default: None. CA key to use for signing dynamically generated '
+                 'HTTPS certificates.  If used, must also pass --ca-cert-file and --ca-signing-key-file'
+        )
+        parser.add_argument(
+            '--ca-cert-dir',
+            type=str,
+            default=DEFAULT_CA_CERT_DIR,
+            help='Default: ~/.proxy.py. Directory to store dynamically generated certificates. '
+                 'Also see --ca-key-file, --ca-cert-file and --ca-signing-key-file'
+        )
+        parser.add_argument(
+            '--ca-cert-file',
+            type=str,
+            default=DEFAULT_CA_CERT_FILE,
+            help='Default: None. Signing certificate to use for signing dynamically generated '
+                 'HTTPS certificates.  If used, must also pass --ca-key-file and --ca-signing-key-file'
+        )
+        parser.add_argument(
+            '--ca-signing-key-file',
+            type=str,
+            default=DEFAULT_CA_SIGNING_KEY_FILE,
+            help='Default: None. CA signing key to use for dynamic generation of '
+                 'HTTPS certificates.  If used, must also pass --ca-key-file and --ca-cert-file'
+        )
+        parser.add_argument(
+            '--cert-file',
+            type=str,
+            default=DEFAULT_CERT_FILE,
+            help='Default: None. Server certificate to enable end-to-end TLS encryption with clients. '
+                 'If used, must also pass --key-file.'
+        )
+        parser.add_argument(
+            '--client-recvbuf-size',
+            type=int,
+            default=DEFAULT_CLIENT_RECVBUF_SIZE,
+            help='Default: 1 MB. Maximum amount of data received from the '
+                 'client in a single recv() operation. Bump this '
+                 'value for faster uploads at the expense of '
+                 'increased RAM.')
+        parser.add_argument(
+            '--devtools-ws-path',
+            type=str,
+            default=DEFAULT_DEVTOOLS_WS_PATH,
+            help='Default: /devtools.  Only applicable '
+                 'if --enable-devtools is used.'
+        )
+        parser.add_argument(
+            '--disable-headers',
+            type=str,
+            default=COMMA.join(DEFAULT_DISABLE_HEADERS),
+            help='Default: None.  Comma separated list of headers to remove before '
+                 'dispatching client request to upstream server.')
+        parser.add_argument(
+            '--disable-http-proxy',
+            action='store_true',
+            default=DEFAULT_DISABLE_HTTP_PROXY,
+            help='Default: False.  Whether to disable proxy.HttpProxyPlugin.')
+        parser.add_argument(
+            '--enable-devtools',
+            action='store_true',
+            default=DEFAULT_ENABLE_DEVTOOLS,
+            help='Default: False.  Enables integration with Chrome Devtool Frontend.'
+        )
+        parser.add_argument(
+            '--enable-events',
+            action='store_true',
+            default=DEFAULT_ENABLE_EVENTS,
+            help='Default: False.  Enables core to dispatch lifecycle events. '
+                 'Plugins can be used to subscribe for core events.'
+        )
+        parser.add_argument(
+            '--enable-static-server',
+            action='store_true',
+            default=DEFAULT_ENABLE_STATIC_SERVER,
+            help='Default: False.  Enable inbuilt static file server. '
+                 'Optionally, also use --static-server-dir to serve static content '
+                 'from custom directory.  By default, static file server serves '
+                 'from public folder.'
+        )
+        parser.add_argument(
+            '--enable-web-server',
+            action='store_true',
+            default=DEFAULT_ENABLE_WEB_SERVER,
+            help='Default: False.  Whether to enable proxy.HttpWebServerPlugin.')
+        parser.add_argument(
+            '--hostname',
+            type=str,
+            default=str(DEFAULT_IPV6_HOSTNAME),
+            help='Default: ::1. Server IP address.')
+        parser.add_argument(
+            '--key-file',
+            type=str,
+            default=DEFAULT_KEY_FILE,
+            help='Default: None. Server key file to enable end-to-end TLS encryption with clients. '
+                 'If used, must also pass --cert-file.'
+        )
+        parser.add_argument(
+            '--log-level',
+            type=str,
+            default=DEFAULT_LOG_LEVEL,
+            help='Valid options: DEBUG, INFO (default), WARNING, ERROR, CRITICAL. '
+                 'Both upper and lowercase values are allowed. '
+                 'You may also simply use the leading character e.g. --log-level d')
+        parser.add_argument('--log-file', type=str, default=DEFAULT_LOG_FILE,
+                            help='Default: sys.stdout. Log file destination.')
+        parser.add_argument('--log-format', type=str, default=DEFAULT_LOG_FORMAT,
+                            help='Log format for Python logger.')
+        parser.add_argument('--num-workers', type=int, default=DEFAULT_NUM_WORKERS,
+                            help='Defaults to number of CPU cores.')
+        parser.add_argument(
+            '--open-file-limit',
+            type=int,
+            default=DEFAULT_OPEN_FILE_LIMIT,
+            help='Default: 1024. Maximum number of files (TCP connections) '
+                 'that proxy.py can open concurrently.')
+        parser.add_argument(
+            '--pac-file',
+            type=str,
+            default=DEFAULT_PAC_FILE,
+            help='A file (Proxy Auto Configuration) or string to serve when '
+                 'the server receives a direct file request. '
+                 'Using this option enables proxy.HttpWebServerPlugin.')
+        parser.add_argument(
+            '--pac-file-url-path',
+            type=str,
+            default=text_(DEFAULT_PAC_FILE_URL_PATH),
+            help='Default: %s. Web server path to serve the PAC file.' %
+                 text_(DEFAULT_PAC_FILE_URL_PATH))
+        parser.add_argument(
+            '--pid-file',
+            type=str,
+            default=DEFAULT_PID_FILE,
+            help='Default: None. Save parent process ID to a file.')
+        parser.add_argument(
+            '--plugins',
+            type=str,
+            default=DEFAULT_PLUGINS,
+            help='Comma separated plugins')
+        parser.add_argument('--port', type=int, default=DEFAULT_PORT,
+                            help='Default: 8899. Server port.')
+        parser.add_argument(
+            '--server-recvbuf-size',
+            type=int,
+            default=DEFAULT_SERVER_RECVBUF_SIZE,
+            help='Default: 1 MB. Maximum amount of data received from the '
+                 'server in a single recv() operation. Bump this '
+                 'value for faster downloads at the expense of '
+                 'increased RAM.')
+        parser.add_argument(
+            '--static-server-dir',
+            type=str,
+            default=DEFAULT_STATIC_SERVER_DIR,
+            help='Default: "public" folder in directory where proxy.py is placed. '
+                 'This option is only applicable when static server is also enabled. '
+                 'See --enable-static-server.'
+        )
+        parser.add_argument(
+            '--threadless',
+            action='store_true',
+            default=DEFAULT_THREADLESS,
+            help='Default: False.  When disabled a new thread is spawned '
+                 'to handle each client connection.'
+        )
+        parser.add_argument(
+            '--timeout',
+            type=int,
+            default=DEFAULT_TIMEOUT,
+            help='Default: ' + str(DEFAULT_TIMEOUT) +
+                 '.  Number of seconds after which '
+                 'an inactive connection must be dropped.  Inactivity is defined by no '
+                 'data sent or received by the client.'
+        )
+        parser.add_argument(
+            '--version',
+            '-v',
+            action='store_true',
+            default=DEFAULT_VERSION,
+            help='Prints proxy.py version.')
+        return parser
+
+    @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)
+
+    @staticmethod
+    def load_plugins(plugins: bytes) -> 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': [],
+        }
+        for plugin_ in plugins.split(COMMA):
+            plugin = text_(plugin_.strip())
+            if plugin == '':
+                continue
+            module_name, klass_name = plugin.rsplit(text_(DOT), 1)
+            klass = getattr(
+                importlib.import_module(
+                    module_name.replace(
+                        os.path.sep, text_(DOT))),
+                klass_name)
+            base_klass = inspect.getmro(klass)[1]
+            p[bytes_(base_klass.__name__)].append(klass)
+            logger.info(
+                'Loaded %s %s.%s',
+                'plugin' if klass.__name__ != 'HttpWebServerRouteHandler' else 'route',
+                module_name,
+                # HttpWebServerRouteHandler route decorator adds a special
+                # staticmethod to return decorated function name
+                klass.__name__ if klass.__name__ != 'HttpWebServerRouteHandler' else klass.name())
+        return p
+
+    @staticmethod
+    def setup_logger(
+            log_file: Optional[str] = DEFAULT_LOG_FILE,
+            log_level: str = DEFAULT_LOG_LEVEL,
+            log_format: str = DEFAULT_LOG_FORMAT) -> None:
+        ll = getattr(
+            logging,
+            {'D': 'DEBUG',
+             'I': 'INFO',
+             'W': 'WARNING',
+             'E': 'ERROR',
+             'C': 'CRITICAL'}[log_level.upper()[0]])
+        if log_file:
+            logging.basicConfig(
+                filename=log_file,
+                filemode='a',
+                level=ll,
+                format=log_format)
+        else:
+            logging.basicConfig(level=ll, format=log_format)
+
+    @staticmethod
+    def is_py3() -> bool:
+        """Exists only to avoid mocking sys.version_info in tests."""
+        return sys.version_info[0] == 3
diff --git a/proxy/common/types.py b/proxy/common/types.py
index 514523a..d749e9b 100644
--- a/proxy/common/types.py
+++ b/proxy/common/types.py
@@ -10,8 +10,8 @@
 import queue
 
 from typing import TYPE_CHECKING, Dict, Any
-from typing_extensions import Protocol
 
+from typing_extensions import Protocol
 
 if TYPE_CHECKING:
     DictQueueType = queue.Queue[Dict[str, Any]]    # pragma: no cover
diff --git a/proxy/main.py b/proxy/main.py
deleted file mode 100755
index cd8e3f8..0000000
--- a/proxy/main.py
+++ /dev/null
@@ -1,250 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-    proxy.py
-    ~~~~~~~~
-    ⚡⚡⚡ Fast, Lightweight, Programmable Proxy Server in a single Python file.
-
-    :copyright: (c) 2013-present by Abhinav Singh and contributors.
-    :license: BSD, see LICENSE for more details.
-"""
-import base64
-import contextlib
-import importlib
-import inspect
-import ipaddress
-import logging
-import multiprocessing
-import os
-import sys
-import time
-import unittest
-from typing import Dict, List, Optional, Generator, Any
-
-from .common.flags import Flags, init_parser
-from .common.utils import text_, bytes_, get_available_port, new_socket_connection
-from .common.types import DictQueueType
-from .common.constants import DOT, COMMA
-from .common.constants import DEFAULT_LOG_FORMAT, DEFAULT_LOG_FILE, DEFAULT_LOG_LEVEL
-from .common.version import __version__
-from .core.acceptor import AcceptorPool
-from .http.handler import HttpProtocolHandler
-
-if os.name != 'nt':
-    import resource
-
-logger = logging.getLogger(__name__)
-
-
-def is_py3() -> bool:
-    """Exists only to avoid mocking sys.version_info in tests."""
-    return sys.version_info[0] == 3
-
-
-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 load_plugins(plugins: bytes) -> 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': [],
-    }
-    for plugin_ in plugins.split(COMMA):
-        plugin = text_(plugin_.strip())
-        if plugin == '':
-            continue
-        module_name, klass_name = plugin.rsplit(text_(DOT), 1)
-        klass = getattr(
-            importlib.import_module(
-                module_name.replace(
-                    os.path.sep, text_(DOT))),
-            klass_name)
-        base_klass = inspect.getmro(klass)[1]
-        p[bytes_(base_klass.__name__)].append(klass)
-        logger.info(
-            'Loaded %s %s.%s',
-            'plugin' if klass.__name__ != 'HttpWebServerRouteHandler' else 'route',
-            module_name,
-            # HttpWebServerRouteHandler route decorator adds a special
-            # staticmethod to return decorated function name
-            klass.__name__ if klass.__name__ != 'HttpWebServerRouteHandler' else klass.name())
-    return p
-
-
-def setup_logger(
-        log_file: Optional[str] = DEFAULT_LOG_FILE,
-        log_level: str = DEFAULT_LOG_LEVEL,
-        log_format: str = DEFAULT_LOG_FORMAT) -> None:
-    ll = getattr(
-        logging,
-        {'D': 'DEBUG',
-         'I': 'INFO',
-         'W': 'WARNING',
-         'E': 'ERROR',
-         'C': 'CRITICAL'}[log_level.upper()[0]])
-    if log_file:
-        logging.basicConfig(
-            filename=log_file,
-            filemode='a',
-            level=ll,
-            format=log_format)
-    else:
-        logging.basicConfig(level=ll, format=log_format)
-
-
-@contextlib.contextmanager
-def start(input_args: List[str]) -> Generator[None, None, None]:
-    if not is_py3():
-        print(
-            'DEPRECATION: "develop" branch no longer supports Python 2.7.  Kindly upgrade to Python 3+. '
-            'If for some reasons you cannot upgrade, consider using "master" branch or simply '
-            '"pip install proxy.py==0.3".'
-            '\n\n'
-            'DEPRECATION: Python 2.7 will reach the end of its life on January 1st, 2020. '
-            'Please upgrade your Python as Python 2.7 won\'t be maintained after that date. '
-            'A future version of pip will drop support for Python 2.7.')
-        sys.exit(1)
-    args = init_parser().parse_args(input_args)
-
-    if args.version:
-        print(__version__)
-        sys.exit(0)
-
-    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)
-
-    try:
-        setup_logger(args.log_file, args.log_level, args.log_format)
-        set_open_file_limit(args.open_file_limit)
-
-        auth_code = None
-        if args.basic_auth:
-            auth_code = b'Basic %s' % base64.b64encode(bytes_(args.basic_auth))
-
-        default_plugins = ''
-        devtools_event_queue: Optional[DictQueueType] = None
-        if args.enable_devtools:
-            default_plugins += 'proxy.http.devtools.DevtoolsProtocolPlugin,'
-            default_plugins += 'proxy.http.server.HttpWebServerPlugin,'
-        if not args.disable_http_proxy:
-            default_plugins += 'proxy.http.proxy.HttpProxyPlugin,'
-        if args.enable_web_server or \
-                args.pac_file is not None or \
-                args.enable_static_server:
-            if 'proxy.http.server.HttpWebServerPlugin' not in default_plugins:
-                default_plugins += 'proxy.http.server.HttpWebServerPlugin,'
-        if args.enable_devtools:
-            default_plugins += 'proxy.http.devtools.DevtoolsWebsocketPlugin,'
-            devtools_event_queue = multiprocessing.Manager().Queue()
-        if args.pac_file is not None:
-            default_plugins += 'proxy.http.server.HttpWebServerPacFilePlugin,'
-
-        flags = Flags(
-            auth_code=auth_code,
-            server_recvbuf_size=args.server_recvbuf_size,
-            client_recvbuf_size=args.client_recvbuf_size,
-            pac_file=bytes_(args.pac_file),
-            pac_file_url_path=bytes_(args.pac_file_url_path),
-            disable_headers=[
-                header.lower() for header in bytes_(
-                    args.disable_headers).split(COMMA) if header.strip() != b''],
-            certfile=args.cert_file,
-            keyfile=args.key_file,
-            ca_cert_dir=args.ca_cert_dir,
-            ca_key_file=args.ca_key_file,
-            ca_cert_file=args.ca_cert_file,
-            ca_signing_key_file=args.ca_signing_key_file,
-            hostname=ipaddress.ip_address(args.hostname),
-            port=args.port,
-            backlog=args.backlog,
-            num_workers=args.num_workers,
-            static_server_dir=args.static_server_dir,
-            enable_static_server=args.enable_static_server,
-            devtools_event_queue=devtools_event_queue,
-            devtools_ws_path=args.devtools_ws_path,
-            timeout=args.timeout,
-            threadless=args.threadless,
-            enable_events=args.enable_events)
-
-        flags.plugins = load_plugins(
-            bytes_(
-                '%s%s' %
-                (default_plugins, args.plugins)))
-
-        acceptor_pool = AcceptorPool(
-            flags=flags,
-            work_klass=HttpProtocolHandler
-        )
-
-        if args.pid_file:
-            with open(args.pid_file, 'wb') as pid_file:
-                pid_file.write(bytes_(os.getpid()))
-
-        try:
-            acceptor_pool.setup()
-            yield
-        except Exception as e:
-            logger.exception('exception', exc_info=e)
-        finally:
-            acceptor_pool.shutdown()
-    except KeyboardInterrupt:  # pragma: no cover
-        pass
-    finally:
-        if args.pid_file and os.path.exists(args.pid_file):
-            os.remove(args.pid_file)
-
-
-class TestCase(unittest.TestCase):
-    """TestCase class that automatically setup and teardown proxy.py."""
-
-    DEFAULT_PROXY_PY_STARTUP_FLAGS = [
-        '--num-workers', '1',
-    ]
-
-    def run(self, result: Optional[unittest.TestResult] = None) -> Any:
-        self.proxy_port = get_available_port()
-
-        flags = getattr(self, 'PROXY_PY_STARTUP_FLAGS') \
-            if hasattr(self, 'PROXY_PY_STARTUP_FLAGS') \
-            else self.DEFAULT_PROXY_PY_STARTUP_FLAGS
-        flags.append('--port')
-        flags.append(str(self.proxy_port))
-
-        with start(flags):
-            # Wait for proxy.py server to come up
-            while True:
-                try:
-                    conn = new_socket_connection(
-                        ('localhost', self.proxy_port))
-                    break
-                except ConnectionRefusedError:
-                    time.sleep(0.1)
-                finally:
-                    conn.close()
-            # Run tests
-            super().run(result)
-
-
-def main(input_args: List[str]) -> None:
-    with start(input_args):
-        # TODO: Introduce cron feature instead of mindless sleep
-        while True:
-            time.sleep(1)
-
-
-def entry_point() -> None:
-    main(sys.argv[1:])
diff --git a/proxy/proxy.py b/proxy/proxy.py
new file mode 100644
index 0000000..4833790
--- /dev/null
+++ b/proxy/proxy.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+"""
+    proxy.py
+    ~~~~~~~~
+    ⚡⚡⚡ Fast, Lightweight, Programmable Proxy Server in a single Python file.
+
+    :copyright: (c) 2013-present by Abhinav Singh and contributors.
+    :license: BSD, see LICENSE for more details.
+"""
+import contextlib
+import os
+import sys
+import time
+import unittest
+import logging
+
+from typing import List, Optional, Generator, Any
+
+from .common.utils import bytes_, get_available_port, new_socket_connection
+from .common.flags import Flags
+from .core.acceptor import AcceptorPool
+from .http.handler import HttpProtocolHandler
+
+logger = logging.getLogger(__name__)
+
+
+class TestCase(unittest.TestCase):
+    """Base TestCase class that automatically setup and teardown proxy.py."""
+
+    DEFAULT_PROXY_PY_STARTUP_FLAGS = [
+        '--num-workers', '1',
+    ]
+
+    def run(self, result: Optional[unittest.TestResult] = None) -> Any:
+        self.proxy_port = get_available_port()
+
+        flags = getattr(self, 'PROXY_PY_STARTUP_FLAGS') \
+            if hasattr(self, 'PROXY_PY_STARTUP_FLAGS') \
+            else self.DEFAULT_PROXY_PY_STARTUP_FLAGS
+        flags.append('--port')
+        flags.append(str(self.proxy_port))
+
+        with start(flags):
+            # Wait for proxy.py server to come up
+            while True:
+                try:
+                    conn = new_socket_connection(
+                        ('localhost', self.proxy_port))
+                    conn.close()
+                    break
+                except ConnectionRefusedError:
+                    time.sleep(0.1)
+            # Run tests
+            super().run(result)
+
+
+@contextlib.contextmanager
+def start(
+        input_args: Optional[List[str]] = None,
+        **opts: Any) -> Generator[None, None, None]:
+    flags = Flags.initialize(input_args, **opts)
+    try:
+        acceptor_pool = AcceptorPool(
+            flags=flags,
+            work_klass=HttpProtocolHandler
+        )
+        if flags.pid_file is not None:
+            with open(flags.pid_file, 'wb') as pid_file:
+                pid_file.write(bytes_(os.getpid()))
+        try:
+            acceptor_pool.setup()
+            yield
+        except Exception as e:
+            logger.exception('exception', exc_info=e)
+        finally:
+            acceptor_pool.shutdown()
+    except KeyboardInterrupt:  # pragma: no cover
+        pass
+    finally:
+        if flags.pid_file and os.path.exists(flags.pid_file):
+            os.remove(flags.pid_file)
+
+
+def main(
+        input_args: Optional[List[str]] = None,
+        **opts: Any) -> None:
+    with start(input_args=input_args, **opts):
+        # TODO: Introduce cron feature instead of mindless sleep
+        while True:
+            time.sleep(1)
+
+
+def entry_point() -> None:
+    main(input_args=sys.argv[1:])
diff --git a/setup.py b/setup.py
index dd48f2c..5adf6d6 100644
--- a/setup.py
+++ b/setup.py
@@ -8,11 +8,16 @@
     :copyright: (c) 2013-present by Abhinav Singh and contributors.
     :license: BSD, see LICENSE for more details.
 """
-from setuptools import setup, find_packages
+from setuptools import setup
 
-from proxy.common.version import __version__
-from proxy.common.constants import __author__, __author_email__
-from proxy.common.constants import __homepage__, __description__, __download_url__, __license__
+VERSION = (2, 0, 0)
+__version__ = '.'.join(map(str, VERSION[0:3]))
+__description__ = '⚡⚡⚡ Fast, Lightweight, Programmable Proxy Server in a single Python file.'
+__author__ = 'Abhinav Singh'
+__author_email__ = 'mailsforabhinav@gmail.com'
+__homepage__ = 'https://github.com/abhinavsingh/proxy.py'
+__download_url__ = '%s/archive/master.zip' % __homepage__
+__license__ = 'BSD'
 
 setup(
     name='proxy.py',
@@ -25,17 +30,11 @@ setup(
     long_description_content_type='text/markdown',
     download_url=__download_url__,
     license=__license__,
-    packages=find_packages(
-        exclude=[
-            'benchmark',
-            'dashboard',
-            'plugin_examples',
-            'tests'
-        ]),
+    packages=['proxy', 'proxy.common', 'proxy.core', 'proxy.http'],
     install_requires=open('requirements.txt', 'r').read().strip().split(),
     entry_points={
         'console_scripts': [
-            'proxy = proxy.main:entry_point'
+            'proxy = proxy:entry_point'
         ]
     },
     classifiers=[


Test patch:
diff --git a/tests/http/test_protocol_handler.py b/tests/http/test_protocol_handler.py
index 37a62b2..6dd1187 100644
--- a/tests/http/test_protocol_handler.py
+++ b/tests/http/test_protocol_handler.py
@@ -22,7 +22,6 @@ from proxy.http.proxy import HttpProxyPlugin
 from proxy.http.parser import httpParserStates, httpParserTypes
 from proxy.http.exception import ProxyAuthenticationFailed, ProxyConnectionFailed
 from proxy.http.handler import HttpProtocolHandler
-from proxy.main import load_plugins
 from proxy.common.version import __version__
 
 
@@ -39,7 +38,7 @@ class TestHttpProtocolHandler(unittest.TestCase):
 
         self.http_server_port = 65535
         self.flags = Flags()
-        self.flags.plugins = load_plugins(
+        self.flags.plugins = Flags.load_plugins(
             b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin')
 
         self.mock_selector = mock_selector
@@ -172,7 +171,7 @@ class TestHttpProtocolHandler(unittest.TestCase):
         flags = Flags(
             auth_code=b'Basic %s' %
                       base64.b64encode(b'user:pass'))
-        flags.plugins = load_plugins(
+        flags.plugins = Flags.load_plugins(
             b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin')
         self.protocol_handler = HttpProtocolHandler(
             self.fileno, self._addr, flags=flags)
@@ -204,7 +203,7 @@ class TestHttpProtocolHandler(unittest.TestCase):
         flags = Flags(
             auth_code=b'Basic %s' %
                       base64.b64encode(b'user:pass'))
-        flags.plugins = load_plugins(
+        flags.plugins = Flags.load_plugins(
             b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin')
 
         self.protocol_handler = HttpProtocolHandler(
@@ -252,7 +251,7 @@ class TestHttpProtocolHandler(unittest.TestCase):
         flags = Flags(
             auth_code=b'Basic %s' %
                       base64.b64encode(b'user:pass'))
-        flags.plugins = load_plugins(
+        flags.plugins = Flags.load_plugins(
             b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin')
 
         self.protocol_handler = HttpProtocolHandler(
diff --git a/tests/http/test_web_server.py b/tests/http/test_web_server.py
index ad459dc..b0352d6 100644
--- a/tests/http/test_web_server.py
+++ b/tests/http/test_web_server.py
@@ -13,7 +13,6 @@ import unittest
 import selectors
 from unittest import mock
 
-from proxy.main import load_plugins
 from proxy.common.flags import Flags
 from proxy.http.handler import HttpProtocolHandler
 from proxy.http.parser import httpParserStates
@@ -32,7 +31,7 @@ class TestWebServerPlugin(unittest.TestCase):
         self._conn = mock_fromfd.return_value
         self.mock_selector = mock_selector
         self.flags = Flags()
-        self.flags.plugins = load_plugins(
+        self.flags.plugins = Flags.load_plugins(
             b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin')
         self.protocol_handler = HttpProtocolHandler(
             self.fileno, self._addr, flags=self.flags)
@@ -92,7 +91,7 @@ class TestWebServerPlugin(unittest.TestCase):
                 events=selectors.EVENT_READ,
                 data=None), selectors.EVENT_READ), ]
         flags = Flags()
-        flags.plugins = load_plugins(
+        flags.plugins = Flags.load_plugins(
             b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin')
         self.protocol_handler = HttpProtocolHandler(
             self.fileno, self._addr, flags=flags)
@@ -145,7 +144,7 @@ class TestWebServerPlugin(unittest.TestCase):
         flags = Flags(
             enable_static_server=True,
             static_server_dir=static_server_dir)
-        flags.plugins = load_plugins(
+        flags.plugins = Flags.load_plugins(
             b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin')
 
         self.protocol_handler = HttpProtocolHandler(
@@ -189,7 +188,7 @@ class TestWebServerPlugin(unittest.TestCase):
                 data=None), selectors.EVENT_WRITE)], ]
 
         flags = Flags(enable_static_server=True)
-        flags.plugins = load_plugins(
+        flags.plugins = Flags.load_plugins(
             b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin')
 
         self.protocol_handler = HttpProtocolHandler(
@@ -223,7 +222,7 @@ class TestWebServerPlugin(unittest.TestCase):
 
     def init_and_make_pac_file_request(self, pac_file: str) -> None:
         flags = Flags(pac_file=pac_file)
-        flags.plugins = load_plugins(
+        flags.plugins = Flags.load_plugins(
             b'proxy.http.proxy.HttpProxyPlugin,proxy.http.server.HttpWebServerPlugin,'
             b'proxy.http.server.HttpWebServerPacFilePlugin')
         self.protocol_handler = HttpProtocolHandler(
diff --git a/tests/test_embed.py b/tests/test_embed.py
index e0091a2..585153e 100644
--- a/tests/test_embed.py
+++ b/tests/test_embed.py
@@ -7,11 +7,11 @@
     :copyright: (c) 2013-present by Abhinav Singh and contributors.
     :license: BSD, see LICENSE for more details.
 """
+from proxy.proxy import TestCase
 from proxy.common.constants import DEFAULT_CLIENT_RECVBUF_SIZE, PROXY_AGENT_HEADER_VALUE
 from proxy.common.utils import socket_connection, build_http_request, build_http_response
 from proxy.http.codes import httpStatusCodes
 from proxy.http.methods import httpMethods
-from proxy.main import TestCase
 
 
 class TestProxyPyEmbedded(TestCase):
diff --git a/tests/test_main.py b/tests/test_main.py
index 6ebb13a..d455026 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -13,8 +13,10 @@ import tempfile
 import os
 
 from unittest import mock
+from typing import List
 
-from proxy.main import main
+from proxy.proxy import main
+from proxy.common.flags import Flags
 from proxy.common.utils import bytes_
 from proxy.http.handler import HttpProtocolHandler
 
@@ -26,7 +28,6 @@ from proxy.common.constants import DEFAULT_CA_CERT_FILE, DEFAULT_CA_KEY_FILE, DE
 from proxy.common.constants import DEFAULT_PAC_FILE, DEFAULT_PLUGINS, DEFAULT_PID_FILE, DEFAULT_PORT
 from proxy.common.constants import DEFAULT_NUM_WORKERS, DEFAULT_OPEN_FILE_LIMIT, DEFAULT_IPV6_HOSTNAME
 from proxy.common.constants import DEFAULT_SERVER_RECVBUF_SIZE, DEFAULT_CLIENT_RECVBUF_SIZE
-from proxy.common.constants import COMMA
 from proxy.common.version import __version__
 
 
@@ -68,66 +69,29 @@ class TestMain(unittest.TestCase):
         mock_args.enable_events = DEFAULT_ENABLE_EVENTS
 
     @mock.patch('time.sleep')
-    @mock.patch('proxy.main.load_plugins')
-    @mock.patch('proxy.main.init_parser')
-    @mock.patch('proxy.main.set_open_file_limit')
-    @mock.patch('proxy.main.Flags')
-    @mock.patch('proxy.main.AcceptorPool')
+    @mock.patch('proxy.proxy.Flags')
+    @mock.patch('proxy.proxy.AcceptorPool')
     @mock.patch('logging.basicConfig')
     def test_init_with_no_arguments(
             self,
             mock_logging_config: mock.Mock,
             mock_acceptor_pool: mock.Mock,
-            mock_protocol_config: mock.Mock,
-            mock_set_open_file_limit: mock.Mock,
-            mock_init_parser: mock.Mock,
-            mock_load_plugins: mock.Mock,
+            mock_flags: mock.Mock,
             mock_sleep: mock.Mock) -> None:
         mock_sleep.side_effect = KeyboardInterrupt()
 
-        mock_args = mock_init_parser.return_value.parse_args.return_value
-        self.mock_default_args(mock_args)
-        main([])
+        input_args: List[str] = []
+        flags = Flags.initialize(input_args=input_args)
+        mock_flags.initialize = lambda *args, **kwargs: flags
 
-        mock_init_parser.assert_called()
-        mock_init_parser.return_value.parse_args.called_with([])
+        main()
 
-        mock_load_plugins.assert_called_with(
-            b'proxy.http.proxy.HttpProxyPlugin,')
         mock_logging_config.assert_called_with(
             level=logging.INFO,
             format=DEFAULT_LOG_FORMAT
         )
-        mock_set_open_file_limit.assert_called_with(mock_args.open_file_limit)
-        mock_protocol_config.assert_called_with(
-            auth_code=mock_args.basic_auth,
-            backlog=mock_args.backlog,
-            ca_cert_dir=mock_args.ca_cert_dir,
-            ca_cert_file=mock_args.ca_cert_file,
-            ca_key_file=mock_args.ca_key_file,
-            ca_signing_key_file=mock_args.ca_signing_key_file,
-            certfile=mock_args.cert_file,
-            client_recvbuf_size=mock_args.client_recvbuf_size,
-            hostname=mock_args.hostname,
-            keyfile=mock_args.key_file,
-            num_workers=0,
-            pac_file=mock_args.pac_file,
-            pac_file_url_path=mock_args.pac_file_url_path,
-            port=mock_args.port,
-            server_recvbuf_size=mock_args.server_recvbuf_size,
-            disable_headers=[
-                header.lower() for header in bytes_(
-                    mock_args.disable_headers).split(COMMA) if header.strip() != b''],
-            static_server_dir=mock_args.static_server_dir,
-            enable_static_server=mock_args.enable_static_server,
-            devtools_event_queue=None,
-            devtools_ws_path=DEFAULT_DEVTOOLS_WS_PATH,
-            timeout=DEFAULT_TIMEOUT,
-            threadless=DEFAULT_THREADLESS,
-            enable_events=DEFAULT_ENABLE_EVENTS,
-        )
         mock_acceptor_pool.assert_called_with(
-            flags=mock_protocol_config.return_value,
+            flags=flags,
             work_klass=HttpProtocolHandler,
         )
         mock_acceptor_pool.return_value.setup.assert_called()
@@ -138,8 +102,8 @@ class TestMain(unittest.TestCase):
     @mock.patch('os.remove')
     @mock.patch('os.path.exists')
     @mock.patch('builtins.open')
-    @mock.patch('proxy.main.init_parser')
-    @mock.patch('proxy.main.AcceptorPool')
+    @mock.patch('proxy.proxy.Flags.init_parser')
+    @mock.patch('proxy.proxy.AcceptorPool')
     def test_pid_file_is_written_and_removed(
             self,
             mock_acceptor_pool: mock.Mock,
@@ -164,58 +128,61 @@ class TestMain(unittest.TestCase):
         mock_remove.assert_called_with(pid_file)
 
     @mock.patch('time.sleep')
-    @mock.patch('proxy.main.Flags')
-    @mock.patch('proxy.main.AcceptorPool')
+    @mock.patch('proxy.proxy.Flags')
+    @mock.patch('proxy.proxy.AcceptorPool')
     def test_basic_auth(
             self,
             mock_acceptor_pool: mock.Mock,
-            mock_protocol_config: mock.Mock,
+            mock_flags: mock.Mock,
             mock_sleep: mock.Mock) -> None:
         mock_sleep.side_effect = KeyboardInterrupt()
-        main(['--basic-auth', 'user:pass'])
-        flags = mock_protocol_config.return_value
+
+        input_args = ['--basic-auth', 'user:pass']
+        flags = Flags.initialize(input_args=input_args)
+        mock_flags.initialize = lambda *args, **kwargs: flags
+
+        main(input_args=input_args)
         mock_acceptor_pool.assert_called_with(
             flags=flags,
             work_klass=HttpProtocolHandler)
         self.assertEqual(
-            mock_protocol_config.call_args[1]['auth_code'],
+            flags.auth_code,
             b'Basic dXNlcjpwYXNz')
 
-    @mock.patch('builtins.print')
-    def test_main_version(
-            self,
-            mock_print: mock.Mock) -> None:
-        with self.assertRaises(SystemExit):
-            main(['--version'])
-            mock_print.assert_called_with(__version__)
-
     @mock.patch('time.sleep')
     @mock.patch('builtins.print')
-    @mock.patch('proxy.main.AcceptorPool')
-    @mock.patch('proxy.main.is_py3')
+    @mock.patch('proxy.proxy.Flags')
+    @mock.patch('proxy.proxy.AcceptorPool')
+    @mock.patch('proxy.proxy.Flags.is_py3')
     def test_main_py3_runs(
             self,
             mock_is_py3: mock.Mock,
             mock_acceptor_pool: mock.Mock,
+            mock_flags: mock.Mock,
             mock_print: mock.Mock,
             mock_sleep: mock.Mock) -> None:
         mock_sleep.side_effect = KeyboardInterrupt()
+
+        input_args = ['--basic-auth', 'user:pass']
+        flags = Flags.initialize(input_args=input_args)
+        mock_flags.initialize = lambda *args, **kwargs: flags
+
         mock_is_py3.return_value = True
-        main([])
+        main(num_workers=1)
         mock_is_py3.assert_called()
         mock_print.assert_not_called()
         mock_acceptor_pool.assert_called()
         mock_acceptor_pool.return_value.setup.assert_called()
 
     @mock.patch('builtins.print')
-    @mock.patch('proxy.main.is_py3')
+    @mock.patch('proxy.proxy.Flags.is_py3')
     def test_main_py2_exit(
             self,
             mock_is_py3: mock.Mock,
             mock_print: mock.Mock) -> None:
         mock_is_py3.return_value = False
         with self.assertRaises(SystemExit) as e:
-            main([])
+            main(num_workers=1)
         mock_print.assert_called_with(
             'DEPRECATION: "develop" branch no longer supports Python 2.7.  Kindly upgrade to Python 3+. '
             'If for some reasons you cannot upgrade, consider using "master" branch or simply '
@@ -227,3 +194,12 @@ class TestMain(unittest.TestCase):
         )
         self.assertEqual(e.exception.code, 1)
         mock_is_py3.assert_called()
+
+    @mock.patch('builtins.print')
+    def test_main_version(
+            self,
+            mock_print: mock.Mock) -> None:
+        with self.assertRaises(SystemExit) as e:
+            main(['--version'])
+            mock_print.assert_called_with(__version__)
+        self.assertEqual(e.exception.code, 0)
diff --git a/tests/test_set_open_file_limit.py b/tests/test_set_open_file_limit.py
index 90788cc..a2be8e6 100644
--- a/tests/test_set_open_file_limit.py
+++ b/tests/test_set_open_file_limit.py
@@ -11,7 +11,7 @@ import os
 import unittest
 from unittest import mock
 
-from proxy.main import set_open_file_limit
+from proxy.common.flags import Flags
 
 if os.name != 'nt':
     import resource
@@ -28,7 +28,7 @@ class TestSetOpenFileLimit(unittest.TestCase):
             self,
             mock_set_rlimit: mock.Mock,
             mock_get_rlimit: mock.Mock) -> None:
-        set_open_file_limit(256)
+        Flags.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))
 
@@ -38,7 +38,7 @@ class TestSetOpenFileLimit(unittest.TestCase):
             self,
             mock_set_rlimit: mock.Mock,
             mock_get_rlimit: mock.Mock) -> None:
-        set_open_file_limit(256)
+        Flags.set_open_file_limit(256)
         mock_get_rlimit.assert_called_with(resource.RLIMIT_NOFILE)
         mock_set_rlimit.assert_not_called()
 
@@ -48,6 +48,6 @@ class TestSetOpenFileLimit(unittest.TestCase):
             self,
             mock_set_rlimit: mock.Mock,
             mock_get_rlimit: mock.Mock) -> None:
-        set_open_file_limit(1024)
+        Flags.set_open_file_limit(1024)
         mock_get_rlimit.assert_called_with(resource.RLIMIT_NOFILE)
         mock_set_rlimit.assert_not_called()