diff --git a/.gitignore b/.gitignore
index aba790683b141a685f8664edb61febe4b63565ee..13747f233fc0bcd0741542af26bf479942f1cf6a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,26 +1,16 @@
 *~
 *.swp
 *.pyc
-bin
-eggs
-parts
-.installed.cfg
-.mr.developer.cfg
 *.egg-info
-src
-develop-eggs
-sphinx/
-html/
-doc/api/
-dist
 .nfs*
-.gdb_history
-build
 .coverage
-record.txt
 *.DS_Store
 .envrc
 coverage.xml
 test_results.xml
 junit-coverage.xml
 .tox/
+html/
+build/
+doc/api/
+dist/
diff --git a/MANIFEST.in b/MANIFEST.in
index 431d9f496ff0a27a584b6d16fe6b3f5430a5adf3..06be307eb9d2cf11d713c42f89356a9a8faa19d9 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,3 +1,3 @@
 include LICENSE README.rst
 recursive-include doc *.rst *.txt *.py *.ico *.png
-recursive-include exposed/tests/data *.cfg
+recursive-include tests/data *.py *.cfg
diff --git a/pyproject.toml b/pyproject.toml
index b1ecc55b0dc8861ee8c6f0e5fea5ce04380350ae..7440a9d4ac9c96928e945f7eec9261a7b1bcdecf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,6 +5,7 @@
 [project]
 name = "exposed"
 version = "1.0.0b0"
+requires-python = ">=3.9"
 description = "Configuration Support for Python Packages and CLIs"
 dynamic = ["readme"]
 license = {text = "BSD 3-Clause License"}
@@ -48,24 +49,20 @@ test = [
     ]
 
 [project.entry-points."exposed.test.config"]
-first = "exposed.tests.data.basic_config"
-first-a = "exposed.tests.data.basic_config:a"
-first-b = "exposed.tests.data.basic_config:b"
-second = "exposed.tests.data.second_config"
-second-b = "exposed.tests.data.second_config:b"
-second-c = "exposed.tests.data.second_config:c"
-complex = "exposed.tests.data.complex"
-complex-var = "exposed.tests.data.complex:cplx"
-verbose-config = "exposed.tests.data.verbose_config"
-error-config = "exposed.tests.data.doesnt_exist"
+first = "tests.data.basic_config"
+first-a = "tests.data.basic_config:a"
+first-b = "tests.data.basic_config:b"
+second = "tests.data.second_config"
+second-b = "tests.data.second_config:b"
+second-c = "tests.data.second_config:c"
+complex = "tests.data.complex"
+complex-var = "tests.data.complex:cplx"
+verbose-config = "tests.data.verbose_config"
+error-config = "tests.data.doesnt_exist"
 
 [tool.setuptools]
-zip-safe = false
-packages = [
-    "exposed",
-    "exposed.tests",
-    "exposed.tests.data",
-    ]
+zip-safe = true
+package-dir = {"" = "src"}
 
 [tool.setuptools.dynamic]
 version = {file = "version.txt"}
@@ -80,11 +77,12 @@ lines_between_types = 1
 [tool.black]
 line-length = 80
 
-[tool.coverage.report]
-omit = ["*/tests/*"]
+[tool.coverage.run]
+relative_files = true
 
 [tool.pytest.ini_options]
 addopts = [
+    "--import-mode=append",
     "--cov-report=term-missing",
     "--cov=exposed",
 ]
diff --git a/exposed/__init__.py b/src/exposed/__init__.py
similarity index 100%
rename from exposed/__init__.py
rename to src/exposed/__init__.py
diff --git a/exposed/click.py b/src/exposed/click.py
similarity index 100%
rename from exposed/click.py
rename to src/exposed/click.py
diff --git a/exposed/config.py b/src/exposed/config.py
similarity index 99%
rename from exposed/config.py
rename to src/exposed/config.py
index d572974a0fd2e8a30107770ab00b89bbdee6eca3..1734000d3e889def81f9f71379ac5e1cb58346b5 100644
--- a/exposed/config.py
+++ b/src/exposed/config.py
@@ -380,4 +380,5 @@ def resource_keys(
             and (not k.name.startswith(strip))
         )
     ]
+    ret_list = list(dict.fromkeys(ret_list))  # order-preserving uniq
     return sorted(ret_list)
diff --git a/exposed/logging.py b/src/exposed/logging.py
similarity index 100%
rename from exposed/logging.py
rename to src/exposed/logging.py
diff --git a/exposed/rc.py b/src/exposed/rc.py
similarity index 100%
rename from exposed/rc.py
rename to src/exposed/rc.py
diff --git a/exposed/tests/__init__.py b/tests/__init__.py
similarity index 100%
rename from exposed/tests/__init__.py
rename to tests/__init__.py
diff --git a/exposed/tests/conftest.py b/tests/conftest.py
similarity index 100%
rename from exposed/tests/conftest.py
rename to tests/conftest.py
diff --git a/exposed/tests/data/__init__.py b/tests/data/__init__.py
similarity index 100%
rename from exposed/tests/data/__init__.py
rename to tests/data/__init__.py
diff --git a/exposed/tests/data/basic_config.py b/tests/data/basic_config.py
similarity index 100%
rename from exposed/tests/data/basic_config.py
rename to tests/data/basic_config.py
diff --git a/exposed/tests/data/complex.py b/tests/data/complex.py
similarity index 100%
rename from exposed/tests/data/complex.py
rename to tests/data/complex.py
diff --git a/exposed/tests/data/oldjson.cfg b/tests/data/oldjson.cfg
similarity index 100%
rename from exposed/tests/data/oldjson.cfg
rename to tests/data/oldjson.cfg
diff --git a/exposed/tests/data/second_config.py b/tests/data/second_config.py
similarity index 100%
rename from exposed/tests/data/second_config.py
rename to tests/data/second_config.py
diff --git a/exposed/tests/data/test_dump_config.py b/tests/data/test_dump_config.py
similarity index 100%
rename from exposed/tests/data/test_dump_config.py
rename to tests/data/test_dump_config.py
diff --git a/exposed/tests/data/test_dump_config2.py b/tests/data/test_dump_config2.py
similarity index 100%
rename from exposed/tests/data/test_dump_config2.py
rename to tests/data/test_dump_config2.py
diff --git a/exposed/tests/data/userdefaults_ex1.cfg b/tests/data/userdefaults_ex1.cfg
similarity index 100%
rename from exposed/tests/data/userdefaults_ex1.cfg
rename to tests/data/userdefaults_ex1.cfg
diff --git a/exposed/tests/data/verbose_config.py b/tests/data/verbose_config.py
similarity index 100%
rename from exposed/tests/data/verbose_config.py
rename to tests/data/verbose_config.py
diff --git a/exposed/tests/test_click.py b/tests/test_click.py
similarity index 96%
rename from exposed/tests/test_click.py
rename to tests/test_click.py
index 985af387bd8a74d66c1c458409053b78e48d9d90..e43d219f02e2565364a8ed425951420574ee1f88 100644
--- a/exposed/tests/test_click.py
+++ b/tests/test_click.py
@@ -266,7 +266,7 @@ def test_resource_option():
         assert a == 1
 
     runner = CliRunner()
-    result = runner.invoke(cli1, ["-a", "exposed.tests.data.basic_config"])
+    result = runner.invoke(cli1, ["-a", "tests.data.basic_config"])
     assert result.exit_code == 0
 
     # test usage without ConfigCommand and without entry_point_group
@@ -288,12 +288,12 @@ def test_resource_option():
         "-a",
         "--a",
         cls=ResourceOption,
-        string_exceptions=("exposed.tests.data.basic_config"),
+        string_exceptions=("tests.data.basic_config"),
         entry_point_group="exposed.test.config",
     )
     def cli3(a):
-        assert a == "exposed.tests.data.basic_config"
+        assert a == "tests.data.basic_config"
 
     runner = CliRunner()
-    result = runner.invoke(cli3, ["-a", "exposed.tests.data.basic_config"])
+    result = runner.invoke(cli3, ["-a", "tests.data.basic_config"])
     assert result.exit_code == 0
diff --git a/exposed/tests/test_config.py b/tests/test_config.py
similarity index 90%
rename from exposed/tests/test_config.py
rename to tests/test_config.py
index 44b7412d01f1a7709a4e05ef3c16ca417eb21b95..5a0a77b92c67fe7b7dde8d81348d38d59ce8d838 100644
--- a/exposed/tests/test_config.py
+++ b/tests/test_config.py
@@ -48,9 +48,9 @@ def test_config_with_module():
 
     c = load(
         [
-            "exposed.tests.data.basic_config",
-            "exposed.tests.data.second_config",
-            "exposed.tests.data.complex",
+            "tests.data.basic_config",
+            "tests.data.second_config",
+            "tests.data.complex",
         ]
     )
     assert hasattr(c, "a") and c.a == 1
@@ -79,7 +79,7 @@ def test_config_with_mixture(datadir):
     c = load(
         [
             datadir / "basic_config.py",
-            "exposed.tests.data.second_config",
+            "tests.data.second_config",
             "complex",
         ],
         entry_point_group="exposed.test.config",
@@ -97,14 +97,14 @@ def test_config_not_found(datadir):
 
 def test_config_load_attribute():
 
-    a = load(["exposed.tests.data.basic_config"], attribute_name="a")
+    a = load(["tests.data.basic_config"], attribute_name="a")
     assert a == 1
 
 
 def test_config_load_no_attribute():
 
     with pytest.raises(ImportError):
-        _ = load(["exposed.tests.data.basic_config"], attribute_name="wrong")
+        _ = load(["tests.data.basic_config"], attribute_name="wrong")
 
 
 @pytest.fixture
@@ -132,7 +132,7 @@ def test_config_click_config_list(cli_messages):
     runner = CliRunner()
     result = runner.invoke(cli, ["list"])
     assert result.exit_code == 0
-    assert result.output.startswith("module: exposed.tests.data")
+    assert result.output.startswith("module: tests.data")
     assert "(cannot be loaded; add another -v for details)" not in result.output
 
 
@@ -142,7 +142,7 @@ def test_config_click_config_list_v(cli_messages):
     runner = CliRunner()
     result = runner.invoke(cli, ["list", "-v"])
     assert result.exit_code == 0
-    assert result.output.startswith("module: exposed.tests.data")
+    assert result.output.startswith("module: tests.data")
     assert "(cannot be loaded; add another -v for details)" in result.output
     assert "[module] Example configuration module" in result.output
 
@@ -153,7 +153,7 @@ def test_config_click_config_list_vv(cli_messages):
     runner = CliRunner()
     result = runner.invoke(cli, ["list", "-vv"])
     assert result.exit_code == 0
-    assert result.output.startswith("module: exposed.tests.data")
+    assert result.output.startswith("module: tests.data")
     assert "(cannot be loaded; add another -v for details)" in result.output
     assert "[module] Example configuration module" in result.output
     assert "NameError" in messages.getvalue()
diff --git a/exposed/tests/test_logging.py b/tests/test_logging.py
similarity index 100%
rename from exposed/tests/test_logging.py
rename to tests/test_logging.py
diff --git a/exposed/tests/test_rc.py b/tests/test_rc.py
similarity index 100%
rename from exposed/tests/test_rc.py
rename to tests/test_rc.py