How to mass-favorite modules on MetaCPAN

I love that MetaCPAN lets me "++" modules (distributions, really) to mark them as favorites, but going distribution by distribution to click things is not my idea of a good time.

Time for laziness, impatience, hubris.

Click favorites? No way! Let's automate that shit!

(First, thanks to Moritz for giving me the pieces of the MetaCPAN API puzzle.)

Step 1: Get a list of distributions I use

Here's how I did it:

  • Use Path::Iterator::Rule (PIR) to find .pm files in @INC that I've used in the last two weeks
  • Convert file names into module names
  • Use the CPAN 02packages.details.txt file to map module names to unique distributions (tarballs)
  • Spew it out so I can save it to a file

Here's the code:

#!/usr/bin/env perl
use v5.14;
use strict;
use warnings;
use PIR;
use Parse::CPAN::Packages;
use HTTP::Tiny;

# how many days history to consider
my $days = shift || 7;

# get 02packages index
say STDERR "Loading 02packages.details";
my $index = "02packages.details.txt.gz";
my $res = HTTP::Tiny->new->mirror("http://www.cpan.org/modules/$index", $index);
if ( ! $res->{success} ) {
    die "Couldn't update $index\n";
}
my $packages = Parse::CPAN::Packages->new($index);

# find recently used .pm files
say STDERR "Finding modules used in within $days day(s)";
my $rule=PIR->new->perl_module->accessed("<$days");
my $iter = $rule->iter( { relative => 1 }, @INC );

my %dists;
while ( my $file = $iter->() ) {
    my $mod = $file =~ s{/}{::}gr =~ s{\.pm$}{}r;
    my $dist = eval { $packages->package($mod)->distribution };
    next unless $dist;
    $dists{$dist->dist} = $dist;
}

say join("     ", $dists{$_}->dist, $dists{$_}->cpanid, $dists{$_}->distvname) for sort keys %dists;

Then I ran it and captured the output:

$ perl recent-mods.pl 14 > dists.txt
Loading 02packages.details
Finding modules used in within 14 day(s)

Step 2: Prune the list

Lots of stuff in that file is deep in the dependency tree weeds.

Unless I recognize the name and think it's actually worth people checking out, I don't want to favorite it.

So I edited the dists.txt file and deleted out lines that I didn't want to favorite.

Step 3: Find my MetaCPAN API token

To favorite dists from a command-line app, I needed my user access token from this URL (you need to be logged into MetaCPAN first): https://api.metacpan.org/user

I looked for a stanza like this:

   "access_token" : [
      {
         "client" : "metacpan.org",
         "token" : "EfABjkq5qU0lXXXb_Be9TdwqB2I"
      }
   ],

Note: that's not my real token. If you don't see yours, log in with one of the social media links (not PAUSE) at https://api.metacpan.org/login

I copied the long "token" value for use in the next section.

Step 4: Post favorites using the API

With the token and list of distribution data, favoriting is just a matter of posting to the API with this program. (I pasted the token in the code, so if you copy this, update it with your own.)

#!/usr/bin/env perl
use v5.14;
use strict;
use warnings;
use HTTP::Tiny;
use JSON;
use Path::Tiny;

my $list = shift
  or die "Usage: $0 <file>\n";

my $token = "EfABjkq5qU0lXXXb_Be9TdwqB2I";

my $ua = HTTP::Tiny->new;

for my $d ( reverse path($list)->lines( { chomp => 1 } ) ) {
    my ($dist, $author, $release) = split ' ', $d;
    my $post = to_json( { distribution => $dist, author => $author, release => $release } );
    my $res = $ua->post(
        "https://api.metacpan.org/user/favorite?access_token=$token",
        { content => $post, headers => {'content-type' => 'application/json' }},
    );
    if ( $res->{success} ) {
        say "Favorited $dist";
    }
    else {
        warn "Could not favorite $dist ($res->{status} $res->{reason})\n";
    }
}

Then I run that with the distribution data:

$ perl vote-favorite.pl dist.txt
...

200 or so distributions later, I was done.

Step 5: Link my MetaCPAN account to my PAUSE account

To get my favorites to show up on my CPAN author's page, I had to link my PAUSE account to my MetaCPAN account on the identities page.

Step 6: Profit! Well... share, anyway

If you're not a CPAN author, I don't think there's yet a place to show off your favorites, but that's OK. Your vote still counts!.

Places like the weekly metacpan report blog will include your votes in their summary.

Now go vote for your favorite CPAN modules!

Posted in cpan, hacks, perl programming | Tagged , , , | Comments closed

No more copy and paste: How to refactor tests with roles

Raise your hand if you've ever cut and paste a huge chunk of code — or even a whole file — for testing. I have. And I feel guilty, because I know the DRY mantra: "Don't repeat yourself!" But somehow, rules we follow for our application code, we forget for our test code.

Here's a recent example from my own Dancer2::Plugin::Queue. It provides a role, Dancer2::Plugin::Queue::Role::Queue, that any particular backend must implement. I wrote a trivial implementation and some tests for it.

Then it was time to write a backend: Dancer2::Plugin::Queue::MongoDB. And write tests for it. (Can you see where this is going?)

The easiest thing would be to copy the .t files from Dancer2::Plugin::Queue to Dancer2::Plugin::Queue::MongoDB. And that's exactly what I didn't want to do.

What did I do instead? I wrote my tests as a role.

What are roles?

If you've used Moose or Moo, then you should already know. If you haven't, then in a nutshell, a Role is a composable unit of behavior. It is not a class, but you can combine roles as part of a class. If you think of inheritance defining an "is-a" relationship, instead a role defines a "does-a" relationship. A class "does" a role, or many roles.

Moose and Moo roles can have attributes and methods just like classes. They can also require methods to be provided by the class that consumes them. This allows for easy, powerful, reusable behavior encapsulation.

Why are roles helpful in testing? When should you use them?

If you think about it, a test describes behavior. Given X, does it do Y? So roles are a very natural way to model and encapsulate tests. A test role can define attributes necessary for conducting the tests (aka "fixtures"), any necessary state initialization, and the tests themselves.

Not every test needs to be written as a role. Even repetitive tests can be written procedurally. Lots of my test files look more or less like this:

use strict;
use warnings;
use Test::More;

my @cases = (
    {
        label => "first case",
        inputs => { ... },
        expected => { ... },
    },
    ...
);

for my $c ( @cases ) {
   # do lots of tests given inputs and expectations
}

Roles are really powerful when you want to test similar behaviors in different *.t files or even different distributions. If the guts of that for loop above would need to be repeated, then that's a good sign that roles might be a good abstraction.

Even then, you might not need roles. There's nothing wrong with a test library containing subroutines. For me, roles are the right choice when I need to start passing a lot of state to my tests, or when I have to do a lot of setup or tear down around my tests.

How can I use roles in testing?

Instead of writing a test library, you could write a test class (or more than one) and use Moose or Moo roles to compose your behaviors. But once you've started to consider that, you should instead jump straight to one of these modules, which make it really easy:

Which one? Either one. I'm slightly biased because I wrote Test::Roo, but I did it because I wanted something like Test::Routine for non-Moose projects where I didn't want Moose as a test dependency. If you get familiar with Test::Roo, then you can use it for either Moose or Moo based projects, which I think is convenient.

Ovid has recently released Test::Class::Moose. I know nothing about it, so can't recommend it, but Ovid is a smart guy who knows a lot about large-scale testing, so it might be worth investigating as well.

When shouldn't I use these modules?

While they offer some nice syntactic sugar even in simple cases, they do add some complexity that might not be worth it for simple tests. While both Moose and Moo are popular, powerful OO frameworks, you might not want to depend on them just for your tests. (Obviously, if your code already uses Moose or Moo, then using Test::Routine or Test::Roo is a no-brainer.)

Examples

I'll give some example from the Test::Roo::Cookbook. These are also in the 'examples' directory of the repository and CPAN distribution.

Intro to Test::Roo

This example introduces you to Test::Roo. It doesn't actually use any roles, but you need to grok this one to follow the later ones. I'll annotate it with comments more heavily than in the original.

# loading Test::Roo loads Moo, strictures and Test::More, and makes the current
# package (main) a subclass of Test::Roo::Class
use Test::Roo;

use MooX::Types::MooseLike::Base qw/ArrayRef/;
use Path::Tiny;

# here is a fixture -- a Moo attribute for a text file to use for testing; note that
# it is 'required', so it must be provided in the constructor arguments 
has corpus => (
    is       => 'ro',
    isa      => sub { -f shift },
    required => 1,
);

# another fixture; this one is lazy and caches lines from the corpus on demand
has lines => (
    is  => 'lazy',
    isa => ArrayRef,
);

# here is the builder that actually loads the lines
sub _build_lines {
    my ($self) = @_;
    return [ map { lc } path( $self->corpus )->lines ];
}

# this is the first test -- whether the lines of the corpus are sorted;  the
# first call to 'lines' caches the lines of the file so it is only read
# once for the life of the test object
test 'sorted' => sub {
    my $self = shift;
    is_deeply( $self->lines, [ sort @{$self->lines} ], "alphabetized");
};

# this is the second test -- whether all letters are accounted for in the corpus;
# it uses the cached lines from the first test
test 'a to z' => sub {
    my $self = shift;
    my %letters = map { substr($_,0,1) => 1 } @{ $self->lines };
    is_deeply( [sort keys %letters], ["a" .. "z"], "all letters found" );
};
 
# this instantiates the current package into an object (with the required argument)
# and runs all the tests
run_me( { corpus => "/usr/share/dict/words" } );

# tell Test::More that there are no more tests
done_testing;

Creating a test role with Test::Roo

Now that you're a little familiar with Test::Roo syntax, I'll give a real example with a test role, based on how I might test a file-finder like Path::Iterator::Rule.

I didn't actually write Path::Iterator::Rule tests like this, because I hadn't written Test::Roo yet. In hindsight, I wish I had. Just writing the example for the cookbook revealed a bug affecting subclasses.

Testing a file finder requires setting up a temporary directory, creating a bunch of files within it, and testing what files or directories are found for a given set of rules. Path::Iterator::Rule was designed to be subclassed, so that I could turn Path::Class::Rule into a subclass that worked with Path::Class objects.

Right away, I can tell that this is good for role-based testing. Lots of state (list of files to create) needed for the test? Check! Lots of setup (creating the files)? Check! Needs to be reusable by subclasses? Check!

I'll discuss the test role file in sections.

First, we set up the test role as a package, load in Test::Roo::Role for a standalone role, and bring in the dependencies:

package IteratorTest;
use Test::Roo::Role;
 
use MooX::Types::MooseLike::Base qw/:all/;
use Class::Load qw/load_class/;
use Path::Tiny;

Then, we define two required attributes -- the iterator class to test and the "result type" that indicates whether files found are objects and if so, of what type.

has [qw/iterator_class result_type/] => (
    is       => 'ro',
    isa      => Str,
    required => 1,
);

Next, we define a list of test files to create and a temporary directory to hold them. While the list has a default value, note that it could be changed during object construction for more specialized testing.

has test_files => (
    is      => 'ro',
    isa     => ArrayRef,
    default => sub {
        return [
            qw(
            aaaa
            bbbb
            cccc/dddd
            eeee/ffff/gggg
            )
        ];
    },
);
 
has tempdir => (
    is  => 'lazy',
    isa => InstanceOf ['Path::Tiny']
);

We also need a object for the iterator we are testing. This one is lazy and will just be an instance of the iterator_class.

 
has rule_object => (
    is      => 'lazy',
    isa     => Object,
    clearer => 1,
);

For all our lazy attributes, we now need some builders. I'll add some extra comments to annotate them.

# gives our Test::More subtest a dynamically determined label
sub _build_description { return shift->iterator_class }

# creates the temporary directory *and* creates all the files in it
sub _build_tempdir {
    my ($self) = @_;
    my $dir = Path::Tiny->tempdir;
    $dir->child($_)->touchpath for @{ $self->test_files };
    return $dir;
}
 
# creates the empty rule object
sub _build_rule_object {
    my ($self) = @_;
    load_class( $self->iterator_class );
    return $self->iterator_class->new;
}

Up to this point, the role has just been defining requirements and fixtures. Next, it's time to define test behaviors.

First, there is a method to test result types, since that needs some different logic depending on whether objects or strings are returned. This isn't a 'test' declaration, but it will be called from them later.

sub test_result_type {
    my ( $self, $file ) = @_;
    if ( my $type = $self->result_type ) {
        isa_ok( $file, $type, $file );
    }
    else {
        is( ref($file), '', "$file is string" );
    }
}

Finally, we get a simple test definition that checks if finding all files gets the same list of files used to create the testing directory.

test 'find files' => sub {
    my $self = shift;
    $self->clear_rule_object; # make sure have a new one each time
 
    $self->tempdir;
    my $rule = $self->rule_object;
    my @files = $rule->file->all( $self->tempdir, { relative => 1 } );
 
    is_deeply( \@files, $self->test_files, "correct list of files" )
    or diag explain \@files;
 
    $self->test_result_type($_) for @files;
};

# ... more tests ...

1;

If we had more tests, we'd add them after that initial test.

If I were doing this "for real", I would refactor the temp directory construction into a separate role, so that I could have different roles for different behaviors without having to repeat my fixture code, either. Roles composing roles FTW! This is a great use for roles even without even considering the benefits for subclass testing.

Given that test role, testing it for a given implementation in a *.t file just looks like this:

use Test::Roo;
use lib 'lib';
 
with 'IteratorTest';
 
run_me(
    {
        iterator_class => 'Path::Iterator::Rule',
        result_type    => '',
    }
);
 
done_testing;

And then the subclass *.t file is the same thing, just with a different set of constructor arguments.

use Test::Roo;
use lib 'lib';
 
with 'IteratorTest';
 
run_me(
    {
        iterator_class => 'Path::Class::Rule',
        result_type    => 'Path::Class::Entity',
    },
);
 
done_testing;

Test role for Dancer2::Plugin::Queue

Now that you've seen the basics of Test::Roo, I'll show you how I wrote my Dancer2::Plugin::Queue test role. For the blog, I'll strip out some the bits less relevant to using test roles (including the actual server setup), but you can see all of it in Dancer2::Plugin::Queue::Role::Test.

package Dancer2::Plugin::Queue::Role::Test;

use Test::Roo::Role;
use MooX::Types::MooseLike::Base qw/Str HashRef CodeRef/;

use Dancer2 ':syntax';
use Dancer2::Plugin::Queue;
use HTTP::Tiny;
use Test::TCP;

has backend => (
    is       => 'ro',
    isa      => Str,
    required => 1,
);

has options => (
    is      => 'lazy',
    isa     => HashRef,
);

sub _build_options { }

has _server => (
    is  => 'lazy',
    isa => CodeRef,
);

sub _build__server {
    my ($self) = @_;
    return sub { ... }; # details elided for the blog post
}

test 'queue and dequeue' => sub {
    my $self = shift;
    test_tcp(
        client => sub {
            my $port = shift;
            my $url  = "http://localhost:$port/";
            my $ua = HTTP::Tiny->new;
            my $res = $ua->get( $url . "add?msg=Hello%20World" );
            like $res->{content}, qr/Hello World/i, "sent and receieved message";
        },
        server => $self->_server,
    );
};

1;

The *.t file just overrides the options builder method, composes the test role, and invokes it for the trivial "Array" backend. It looks like this:

use Test::Roo;
 
sub _build_options { return { name => 'foo' } }
 
with 'Dancer2::Plugin::Queue::Role::Test';
 
run_me( { backend => 'Array' } );
 
done_testing;

Summary

If you don't like copy-and-paste coding, roles are a great way to compose behaviors for testing. If you're already used to Moose or Moo — or using them in your code — now it's easy to use roles for testing, too.

Oh, yeah, remember that queue plugin, Dancer2::Plugin::Queue::MongoDB, the one where I was thinking to copy and paste the test? I've already shown you the test role. Now I'll show you how I used it.

Most of the code below is just fixture setup — making sure there is a MongoDB and clearing a test database for the test run. Again, I'll annotate it a bit more than the original.

use Test::Roo;
use MooX::Types::MooseLike::Base qw/:all/;

use MongoDB 0.45;
use MongoDBx::Queue;

# We use this client for checking for a DB and dropping tables before a run;
# it's lazy so we can test creating it in an eval and skip tests otherwise
has client => (
    is => 'lazy',
    isa => InstanceOf['MongoDB::MongoClient'],
);

sub _build_client {
    MongoDB::MongoClient->new;
}

has db_name => (
    is => 'ro',
    isa => Str,
    default => sub { 'test_dancer_plugin_queue_mongodb' },
);

# this overrides the test role's builder to set the right options in the server
# for this backend implementation
sub _build_options {
    my ($self) = @_;
    return { db_name => $self->db_name };
}

# Here's where we actually check that the database exists.  We try to
# build the lazy client attribute, and skip tests if it fails.
sub BUILD {
    my ($self) = @_;
    eval { $self->client }
        or plan skip_all => "No MongoDB on localhost";
}

# I didn't show this part of Test::Roo, but this runs before any tests
# to ensure the test table is dropped
before setup => sub {
    my $self = shift;
    my $db   = $self->client->get_database($self->db_name);
    my $coll = $db->get_collection('queue');
    $coll->drop;
};

# This runs after all tests to clean up
after teardown => sub {
    my $self = shift;
    my $db   = $self->client->get_database($self->db_name);
    $db->drop;
};

# This composes the test role for us to run
with 'Dancer2::Plugin::Queue::Role::Test';

run_me({ backend => 'MongoDB' });
done_testing;

See? The .t files is all fixture management and the test behaviors stay in the role from the superclass. As the test role gets better or bugs are fixed, the tests for the implementations get better, too.

I don't have to copy and paste a thing.

My advice to you

If you made it to the end of this long article, I'll guess that you're intrigued or maybe even excited. Here's what you need to do next:

  • Install Test::Roo. Go on. Do it. Right now.
  • Copy the first example from the cookbook into a file. (Install Path::Tiny while you're at it.)
  • Run it! Then tweak the code and run it again.

That's all you need to get started. Try splitting the test from that file out into a role in another .pm file and compose it back to the .t file using with. Run it again!

Now that you see how it works, the next time you are about to cut and paste some test code, try roles instead.

Good luck, and stay DRY!

Additional Reading

Posted in perl programming | Tagged , , , | Comments closed

Stop Pod tests before they stop you!

Have you ever installed a CPAN module with a big dependency chain and had installation fail somewhere in the middle? Have you ever investigated and found the failure was due to Test::Pod or Test::Pod::Coverage?

AAARRRGGGHHH!

I hate that! Pod tests are release tests and shouldn't be inflicted on end users. But some authors got hooked on a crazy fad of bundling those tests. (I blame you, Module::Starter.)

I've figured out a hack to stop those annoying tests: TAP::Harness::Restricted.

It bypasses Pod and Pod coverage tests on the fly. All you need to do is specify it in the HARNESS_SUBCLASS environment variable.

$ cpanm TAP::Harness::Restricted
$ export HARNESS_SUBCLASS=TAP::Harness::Restricted
$ cpanm Thing::With::Lots::Of::Dependencies

Never be bothered by failing Pod tests again!

Posted in hacks, perl programming | Tagged , , , , | Comments closed

How I'm using Dist::Zilla to give credit to contributors

Recently, Gabor Szabo asked me how to list contributors in the META file of CPAN distributions. This seemed like a great idea to me -- I'd love to be able to credit people who contribute to my modules. I suggested using an "x_contributors" key in META.json with an array of names and email addresses, just like the "author" field has.

Unfortunately, I'm just lazy enough that I wouldn't want to maintain a contributors list by hand. At best, I note additions in Changes. If you're lucky. And catch me on a good day.

Fortunately, I love Dist::Zilla. I let it automate as much as I can of my distribution packaging, documentation and release process.

If you don't love Dist::Zilla, too, Gabor has written a tutorial on adding contributors to CPAN META files including other build systems.

As I was emailing back to Gabor, I realized that the easy thing to do is get contributors from the commit history. I looked on CPAN to see if anyone was doing something like that already.

Bingo! There was Dist::Zilla::Plugin::ContributorsFromGit waiting for me! That plugin, when combined with Pod::Weaver and Pod::Weaver::Section::Contributors, automatically grabs the commit authors list and creates a "CONTRIBUTORS" section in pod. All that was missing was dumping that same data into the META file. One pull request later, Chris Weyl shipped a new version that did just that.

Now, every distribution I release with my Dist::Zilla plugin bundle automatically credits my contributors both in the META files and the Pod.

You can see examples here and here.

If websites like metacpan.org start using the "x_contributors" key for some cool mashup, all my recent distributions will be ready and waiting.

The rest of this article will show you how you can do this, too.

An example distribution

I'm not going to give a full Dist::Zilla (dzil) tutorial here. If you're new to dzil, go see the Dist::Zilla tutorial site.

For those who know how I use dzil, I'm showing how to do this long-hand without my plugin bundle -- that would just confuse people.

You can follow along from this git repository: http://github.com/dagolden/zzz-givecreditwithdzil

First, I created a new distribution:

$ dzil new Acme::GiveCreditWithDzil

Then, I edited the dist.ini file for a pretty minimal dzil setup. It automatically sets a date-based version number. It creates a META.json. It has the PodWeaver plugin to automatically generate Pod sections like AUTHOR and LICENSE. It generates a README from the main module Pod. (Browse the dist.ini on github.)

Next, I wanted to add some committers. Normally, they'd send pull requests, but for this demonstration, I can add them with empty commits:

$ git commit --allow-empty --author="Ricardo Signes <rjbs@cpan.org>" -m "..."

This is really useful! If you have contributors on a project who didn't send you pull requests, you can still record the fact of their contribution in your git history and let dzil automatically include them in the contributors list later.

I added a few more:

$ git commit --allow-empty --author="Keedi Kim - 김도형 <keedi@cpan.org>" -m "..."
$ git commit --allow-empty --author="Chris Weyl <cweyl@alumni.drew.edu>" -m "..."
$ git commit --allow-empty --author="Rik Signes <www@rjbs.manxome.org>" -m "..."

Note that Keedi has Unicode in the author name. Also note that Ricardo is there twice, with different spellings and email addresses.

Next, I added the ContributorsFromGit plugin to the dist.ini:

--- a/dist.ini
+++ b/dist.ini
@@ -16,6 +16,9 @@ copyright_year   = 2013
 ; add $VERSION to module
 [PkgVersion]

+; gather contributors
+[ContributorsFromGit]
+
 ; generate pod sections
 [PodWeaver]

Then, after running dzil build, my META.json had a section that looked like this:

   "x_contributors" : [
      "Chris Weyl <cweyl@alumni.drew.edu>",
      "Keedi Kim - \u00ea\u00b9\u0080\u00eb\u008f\u0084\u00ed\u0098\u0095 <keedi@cpan.org>",
      "Ricardo Signes <rjbs@cpan.org>",
      "Rik Signes <www@rjbs.manxome.org>"
   ]

You can see the duplicate entries for Ricardo, but I'll show you how I fixed that later.

Next, I added the Pod::Weaver::Section::Contributors plugin to my weaver.ini file:

diff --git a/weaver.ini b/weaver.ini
index 7223daf..18494c2 100644
--- a/weaver.ini
+++ b/weaver.ini
@@ -2,3 +2,5 @@

 [-Transformer]
 transformer = List
+
+[Contributors]

Then, running dzil build created a new Pod section. Looking at the README generated from Pod, I saw this:

CONTRIBUTORS
    *   Chris Weyl <cweyl@alumni.drew.edu>

    *   Keedi Kim - 김도형 <keedi@cpan.org>

    *   Ricardo Signes <rjbs@cpan.org>

    *   Rik Signes <www@rjbs.manxome.org>

Unfortunately, Pod::Weaver isn't very smart about Unicode by default, so I got this when I ran perldoc on the generated file:

CONTRIBUTORS
       ·   Chris Weyl <cweyl@alumni.drew.edu>

       ·   Keedi Kim - ê¹<U+0080>ë<U+008F><U+0084>í<U+0098><U+0095> <keedi@cpan.org>

       ·   Ricardo Signes <rjbs@cpan.org>

       ·   Rik Signes <www@rjbs.manxome.org>

Oops.

I fixed that and Rik's name with a somewhat undocumented feature of git: the .mailmap file. Put simply, it remaps commit author name and email address.

Here's one I created to map Ricardo's commits together and strip the Unicode characters from Keedi (sorry!):

Keedi Kim <keedi@cpan.org>
Ricardo Signes <rjbs@cpan.org> <www@rjbs.manxome.org>

After that, here's what I got from perldoc:

CONTRIBUTORS
       ·   Chris Weyl <cweyl@alumni.drew.edu>

       ·   Keedi Kim <keedi@cpan.org>

       ·   Ricardo Signes <rjbs@cpan.org>

Excellent!

I've since learned that I can add "=encoding utf-8" to the top of my Pod and the Unicode bits will work, but I'm still leery of Pod::Weaver and UTF-8, so I tend not to rely on it.

Stepping back

I walked you through that step-by-step, but if I look at the diff between the original distribution and the one that reports contributors, the difference is two plugins and a .mailmap file:

diff --git a/.mailmap b/.mailmap
new file mode 100644
index 0000000..f229d28
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,2 @@
+Keedi Kim <keedi@cpan.org>
+Ricardo Signes <rjbs@cpan.org> <www@rjbs.manxome.org>
diff --git a/dist.ini b/dist.ini
index 4c0a7d8..a501a49 100644
--- a/dist.ini
+++ b/dist.ini
@@ -16,6 +16,9 @@ copyright_year   = 2013
 ; add $VERSION to module
 [PkgVersion]

+; gather contributors
+[ContributorsFromGit]
+
 ; generate pod sections
 [PodWeaver]

diff --git a/weaver.ini b/weaver.ini
index 7223daf..18494c2 100644
--- a/weaver.ini
+++ b/weaver.ini
@@ -2,3 +2,5 @@

 [-Transformer]
 transformer = List
+
+[Contributors]

That's four non-whitespace lines.

If you use Dist::Zilla, giving credit to contributors is that easy!

You've got no excuse. Even if you don't put Contributors in Pod, use ContributorsFromGit and put it into your metadata. Let's give credit where credit is due.

Posted in Uncategorized | Comments closed

HTTP::Tiny now with cookies

In early March, the NY Perl Mongers hosted the first NY.pm hackathon in a space generously provided by the Rubenstein Technology Group.

One of the projects I was pleased to see completed was adding cookie jar support to HTTP::Tiny. A few weeks prior, I had released HTTP::CookieJar in preparation. At the event, Edward Zborowski from Rubenstein volunteered to bring the two together.

Thank you, Edward!

Now, persistent cookie support for HTTP::Tiny is as easy as this:

use HTTP::Tiny;
use HTTP::CookieJar;
use Path::Tiny;

my $jar_file = path("jar.txt");

$jar_file->touch;

my $jar = HTTP::CookieJar->new->load_cookies( $jar_file->lines );

my $ua = HTTP::Tiny->new( cookie_jar => $jar );

# ... do stuff that needs cookies ...

$jar_file->spew( join "\n", $jar->dump_cookies( {persistent => 1} ) );
Posted in perl programming | Tagged , , , , | Comments closed

© 2009-2014 David Golden All Rights Reserved