larsoner: Sounds good to me. This broke the `test_main.py` test. It would also be good to add a test for this to `.travis.yml`, maybe by calling `numpydoc --validate numpydoc.<something>` and asserting that it works for something good (zero exit code and no output) and for something bad (non-zero exit code and some output)
datapythonista: Tests are passing now. Not sure exactly what was the goal of mocking modules in the original tests, but I reimplemented all tests in a way that seems simpler to me, and that everything is being tested. I also added calls to `python -m numpydoc` to travis directly to check the status codes.

I don't think the coverage warning is relevant, I think it's being caused by the lack of tests of the `argparse` stuff in `if __name__ == '__main__':`. I'm sure we don't want to test that.
datapythonista: @larsoner did you have a chance to look at this? I think this should be ready, let me know if it's not.
datapythonista: Yes, I think ignoring is complex enough to go into a separate PR. We probably want to specify it in `setup.cfg` too, and whether is better to whitelist or blacklist error codes may not be obvious.
larsoner: Okay let's get this in and keep iterating, thanks @datapythonista 
diff --git a/.travis.yml b/.travis.yml
index c1aa853..f6dd4cc 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -23,6 +23,12 @@ script:
     cd dist
     pip install numpydoc* -v
   - pytest -v --pyargs numpydoc
+  # Making sure the command line options work
+  - python -m numpydoc numpydoc.tests.test_main._capture_stdout
+  - echo '! python -m numpydoc numpydoc.tests.test_main._invalid_docstring' | bash
+  - python -m numpydoc --validate numpydoc.tests.test_main._capture_stdout
+  - echo '! python -m numpydoc --validate numpydoc.tests.test_main._docstring_with_errors' | bash
+  # Build documentation
   - |
     cd ../doc
     make SPHINXOPTS=$SPHINXOPTS html
diff --git a/doc/validation.rst b/doc/validation.rst
index c851668..3672d83 100644
--- a/doc/validation.rst
+++ b/doc/validation.rst
@@ -2,11 +2,16 @@
 Validating NumpyDoc docstrings
 ==============================
 
-One tool for validating docstrings is to see how an object's dosctring
-translates to Restructured Text.  Using numpydoc as a command-line tool
-facilitates this. For example to see the Restructured Text generated
-for ``numpy.ndarray``, use:
+To see the Restructured Text generated for an object, the ``numpydoc`` module
+can be called. For example, to do it for ``numpy.ndarray``, use:
 
 .. code-block:: bash
 
     $ python -m numpydoc numpy.ndarray
+
+This will validate that the docstring can be built.
+
+For an exhaustive validation of the formatting of the docstring, use the
+``--validate`` parameter. This will report the errors detected, such as
+incorrect capitalization, wrong order of the sections, and many other
+issues.
diff --git a/numpydoc/__init__.py b/numpydoc/__init__.py
index da8b80a..8058bb5 100644
--- a/numpydoc/__init__.py
+++ b/numpydoc/__init__.py
@@ -1,3 +1,7 @@
+"""
+This package provides the numpydoc Sphinx extension for handling docstrings
+formatted according to the NumPy documentation format.
+"""
 __version__ = '1.0.0.dev0'
 
 
diff --git a/numpydoc/__main__.py b/numpydoc/__main__.py
index 25f58cf..534e9e0 100644
--- a/numpydoc/__main__.py
+++ b/numpydoc/__main__.py
@@ -1,13 +1,32 @@
+"""
+Implementing `python -m numpydoc` functionality.
+"""
+import sys
 import argparse
-import importlib
 import ast
 
 from .docscrape_sphinx import get_doc_object
+from .validate import validate, Docstring
 
 
-def main(argv=None):
+def render_object(import_path, config=None):
     """Test numpydoc docstring generation for a given object"""
+    # TODO: Move Docstring._load_obj to a better place than validate
+    print(get_doc_object(Docstring(import_path).obj,
+                         config=dict(config or [])))
+    return 0
 
+
+def validate_object(import_path):
+    exit_status = 0
+    results = validate(import_path)
+    for err_code, err_desc in results["errors"]:
+        exit_status += 1
+        print(':'.join([import_path, err_code, err_desc]))
+    return exit_status
+
+
+if __name__ == '__main__':
     ap = argparse.ArgumentParser(description=__doc__)
     ap.add_argument('import_path', help='e.g. numpy.ndarray')
 
@@ -20,25 +39,13 @@ def main(argv=None):
                     action='append',
                     help='key=val where val will be parsed by literal_eval, '
                          'e.g. -c use_plots=True. Multiple -c can be used.')
-    args = ap.parse_args(argv)
+    ap.add_argument('--validate', action='store_true',
+                    help='validate the object and report errors')
+    args = ap.parse_args()
 
-    parts = args.import_path.split('.')
-
-    for split_point in range(len(parts), 0, -1):
-        try:
-            path = '.'.join(parts[:split_point])
-            obj = importlib.import_module(path)
-        except ImportError:
-            continue
-        break
+    if args.validate:
+        exit_code = validate_object(args.import_path)
     else:
-        raise ImportError('Could not resolve {!r} to an importable object'
-                          ''.format(args.import_path))
-
-    for part in parts[split_point:]:
-        obj = getattr(obj, part)
+        exit_code = render_object(args.import_path, args.config)
 
-    print(get_doc_object(obj, config=dict(args.config or [])))
-
-if __name__ == '__main__':
-    main()
+    sys.exit(exit_code)
diff --git a/numpydoc/validate.py b/numpydoc/validate.py
index fe0473c..1fd80e1 100644
--- a/numpydoc/validate.py
+++ b/numpydoc/validate.py
@@ -152,7 +152,7 @@ class Docstring:
         >>> Docstring._load_obj('datetime.datetime')
         <class 'datetime.datetime'>
         """
-        for maxsplit in range(1, name.count(".") + 1):
+        for maxsplit in range(0, name.count(".") + 1):
             module, *func_parts = name.rsplit(".", maxsplit)
             try:
                 obj = importlib.import_module(module)

Validation is not strict enough
The [docs suggest](https://numpydoc.readthedocs.io/en/latest/validation.html) that `python -m numpydoc` can be used to validate docstrings.  However, `numpydoc` reports only the most egregious breakages of the standard.

If we want to help users write high quality docstrings, we should provide a stronger checker.

In the following example, only the incorrect header gets caught:

```
def foo(x):
    """The first line should be a one line description, not a multiple
    paragraph sentence.

    References
    ----------
    These are out of place and incorrectly formatted.

    Some Heading
    ------------
    Unknown heading

    Parameters
    ----------
    x :
    I should specify a type for this parameter

    """
    return x
```


diff --git a/numpydoc/tests/test_main.py b/numpydoc/tests/test_main.py
index e565bb2..d7f6657 100644
--- a/numpydoc/tests/test_main.py
+++ b/numpydoc/tests/test_main.py
@@ -1,80 +1,113 @@
-from __future__ import print_function
-
-from contextlib import contextmanager
-import os
 import sys
-import tempfile
-try:
-    from StringIO import StringIO
-except ImportError:
-    from io import StringIO
+import io
+import pytest
+import numpydoc
+import numpydoc.__main__
 
-from numpydoc.__main__ import main
 
+def _capture_stdout(func_name, *args, **kwargs):
+    """
+    Return stdout of calling `func_name`.
 
-PACKAGE_CODE = """
-'''This package has test stuff'''
-"""
+    This docstring should be perfect, as it is used to test the
+    validation with a docstring without errors.
+
+    Parameters
+    ----------
+    func_name : callable
+        Function to be called.
+    *args, **kwargs
+        Will be passed to `func_name`.
+
+    Returns
+    -------
+    str
+        The content that the function printed.
+
+    See Also
+    --------
+    sys.stdout : Python's file handler for stdout.
+
+    Examples
+    --------
+    >>> _capture_stdout(print, 'hello world')
+    'hello world'
+    """
+    f = io.StringIO()
+    sys.stdout, old_stdout = f, sys.stdout
+    try:
+        func_name(*args, **kwargs)
+        return f.getvalue().strip('\n\r')
+    finally:
+        sys.stdout = old_stdout
 
-MODULE_CODE = """
-'''This module has test stuff'''
 
-def foo(a, b=5):
-    '''Hello world
+def _docstring_with_errors():
+    """
+    this docstring should report some errors
 
     Parameters
     ----------
-    something : foo
-        bar
-    something_else
-        bar
-    '''
-"""
+    made_up_param : str
+    """
+    pass
 
 
-@contextmanager
-def _mock_module(pkg_name):
-    try:
-        tempdir = tempfile.mkdtemp()
-        os.mkdir(os.path.join(tempdir, pkg_name))
-        with open(os.path.join(tempdir, pkg_name, '__init__.py'), 'w') as f:
-            print(PACKAGE_CODE, file=f)
-        with open(os.path.join(tempdir, pkg_name, 'module.py'), 'w') as f:
-            print(MODULE_CODE, file=f)
-
-        sys.path.insert(0, tempdir)
-        yield tempdir
-    finally:
-        try:
-            os.path.rmdir(tempdir)
-            sys.path.remove(tempdir)
-        except:
-            pass
+def _invalid_docstring():
+    """
+    This docstring should break the parsing.
 
+    See Also
+    --------
+    : this is invalid
+    """
+    pass
 
-def _capture_main(*args):
-    f = StringIO()
-    sys.stdout, old_stdout = f, sys.stdout
-    try:
-        main(args)
-        return f.getvalue().strip('\n\r')
-    finally:
-        sys.stdout = old_stdout
 
+def test_renders_package_docstring():
+    out = _capture_stdout(numpydoc.__main__.render_object,
+                          'numpydoc')
+    assert out.startswith('This package provides the numpydoc Sphinx')
+
+
+def test_renders_module_docstring():
+    out = _capture_stdout(numpydoc.__main__.render_object,
+                          'numpydoc.__main__')
+    assert out.startswith('Implementing `python -m numpydoc` functionality.')
+
+
+def test_renders_function_docstring():
+    out = _capture_stdout(numpydoc.__main__.render_object,
+                          'numpydoc.tests.test_main._capture_stdout')
+    assert out.startswith('Return stdout of calling')
+
+
+def test_render_object_returns_correct_exit_status():
+    exit_status = numpydoc.__main__.render_object(
+        'numpydoc.tests.test_main._capture_stdout')
+    assert exit_status == 0
+
+    with pytest.raises(numpydoc.docscrape.ParseError):
+        numpydoc.__main__.render_object(
+            'numpydoc.tests.test_main._invalid_docstring')
+
+
+def test_validate_detects_errors():
+    out = _capture_stdout(numpydoc.__main__.validate_object,
+                          'numpydoc.tests.test_main._docstring_with_errors')
+    assert 'SS02' in out
+    assert 'Summary does not start with a capital letter' in out
+
+    exit_status = numpydoc.__main__.validate_object(
+        'numpydoc.tests.test_main._docstring_with_errors')
+    assert exit_status > 0
 
-def test_main():
-    # TODO: does not currently check that numpydoc transformations are applied
 
-    assert (_capture_main('numpydoc.__main__.main') ==
-            main.__doc__.strip())
+def test_validate_perfect_docstring():
+    out = _capture_stdout(numpydoc.__main__.validate_object,
+                          'numpydoc.tests.test_main._capture_stdout')
+    assert out == ''
 
-    # check it works with modules not imported from __init__
-    with _mock_module('somepackage1'):
-        out = _capture_main('somepackage1.module.foo')
-    assert out.startswith('Hello world\n')
-    with _mock_module('somepackage2'):
-        out = _capture_main('somepackage2.module')
-    assert out.startswith('This module has test')
-    with _mock_module('somepackage3'):
-        out = _capture_main('somepackage3')
-    assert out.startswith('This package has test')
+    exit_status = numpydoc.__main__.validate_object(
+        'numpydoc.tests.test_main._capture_stdout')
+    assert exit_status == 0

pip install -e .
parse_log_pytest
pytest
numpy>=1.16.0 matplotlib>=3.0.0 pytest pytest-cov codecov
apt-get update apt-get install -y gcc texlive texlive-latex-extra latexmk dvipng
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
attrs @ file:///croot/attrs_1668696182826/work
Babel==2.14.0
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
codecov==2.1.13
coverage==7.2.7
cycler==0.11.0
docutils==0.19
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
fonttools==4.38.0
idna==3.10
imagesize==1.4.1
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1648562407465/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
Jinja2==3.1.6
kiwisolver==1.4.5
MarkupSafe==2.1.5
matplotlib==3.5.3
numpy==1.21.6
-e git+https://github.com/numpy/numpydoc.git@472898ea0ca15561273fae1bf00468cd4117ea2b#egg=numpydoc
packaging @ file:///croot/packaging_1671697413597/work
Pillow==9.5.0
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
Pygments==2.17.2
pyparsing==3.1.4
pytest==7.1.2
pytest-cov==4.1.0
python-dateutil==2.9.0.post0
pytz==2025.2
requests==2.31.0
six==1.17.0
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
typing_extensions @ file:///croot/typing_extensions_1669924550328/work
urllib3==2.0.7
zipp @ file:///croot/zipp_1672387121353/work

name: numpydoc
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib-metadata=4.11.3=py37h06a4308_0
  - importlib_metadata=4.11.3=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - typing_extensions=4.4.0=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - babel==2.14.0
      - charset-normalizer==3.4.1
      - codecov==2.1.13
      - coverage==7.2.7
      - cycler==0.11.0
      - docutils==0.19
      - fonttools==4.38.0
      - idna==3.10
      - imagesize==1.4.1
      - jinja2==3.1.6
      - kiwisolver==1.4.5
      - markupsafe==2.1.5
      - matplotlib==3.5.3
      - numpy==1.21.6
      - pillow==9.5.0
      - pygments==2.17.2
      - pyparsing==3.1.4
      - pytest-cov==4.1.0
      - python-dateutil==2.9.0.post0
      - pytz==2025.2
      - requests==2.31.0
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - urllib3==2.0.7
prefix: /opt/conda/envs/numpydoc

numpydoc/tests/test_main.py::test_renders_package_docstring numpydoc/tests/test_main.py::test_renders_module_docstring numpydoc/tests/test_main.py::test_renders_function_docstring numpydoc/tests/test_main.py::test_render_object_returns_correct_exit_status numpydoc/tests/test_main.py::test_validate_detects_errors numpydoc/tests/test_main.py::test_validate_perfect_docstring
numpydoc/tests/test_main.py::test_main._capture_stdout
numpydoc/__main__.py numpydoc/__init__.py numpydoc/validate.py .travis.yml doc/validation.rst
numpydoc/__main__.py numpydoc/__init__.py numpydoc/validate.py .travis.yml doc/validation.rst